Carbon Calculator access gates
Before a Moodle user can see anything in the Carbon Calculator, they pass a chain of gates: their School account must be linked to a company, that company must have a valid UK Companies House number (CRN), the company may have to be flagged as a CC Partner or S3 Partner, and the user must hold a Carbon-eligible Moodle role. Every denial is written to an append-only log.
Why it exists
Section titled “Why it exists”The Carbon Calculator was rolled out in phases to selected partner
organisations, and it keys all data off the company’s CRN — a report
belongs to a company, not a person, and suppliers are matched to
companies by CRN. A user with no company, or a company with a placeholder
CRN like 00000000, would create orphaned or colliding data. The gates
enforce that up front rather than failing deeper in.
Entry points
Section titled “Entry points”| Path | What it does |
|---|---|
wp-content/plugins/carboncalc/includes/LMS.php:119 | carbon_company_access_gate_detail() — the company/CRN/partner gate |
wp-content/plugins/carboncalc/includes/LMS.php:448 | carbon_login_blocked_no_permitted_carbon_role() — role gate |
wp-content/themes/supplychainschool/inc/classes/LMSClient.php:1263 | Gate invocation during login; redirects on block |
wp-content/themes/carboncalculator/functions.php:797 | carboncalculator_enforce_company_crn_for_lms_session() on template_redirect (priority 8) |
wp-content/plugins/carboncalc/includes/Rest.php:207 | must_have_company() — permission callback for nearly every REST route |
wp-content/plugins/carboncalc/includes/LoginAudit.php:24 | log_access_denied() → wp-content/carboncalc-login-denied.log |
wp-content/plugins/carboncalc/includes/AdminLoginLogPage.php | wp-admin → Carbon Calculator → Carbon access log |
How it works
Section titled “How it works”The gate runs at three points: during login (LMSClient::log_in), on
every Carbon page load (template_redirect), and on every REST request
(must_have_company). All three call into
CarbonCalc\LMS::carbon_company_access_gate_detail().
flowchart TD
A[lms_user_id] --> B{cc_users row exists}
B -->|no| R1[no_cc_users_row]
B -->|yes| C{moodle_company_id set}
C -->|no| R2[no_moodle_company_on_user]
C -->|yes| D{cc_company row exists}
D -->|no| D2[sync_roles_for_email then stub row]
D2 -->|still none| R3[no_cc_company_row]
D -->|yes| E{CRN valid}
E -->|no + setting on| R4[missing_crn or invalid_crn]
E -->|ok| F{partners_only setting}
F -->|on and not partner| R5[not_cc_partner]
F -->|off or partner| G{s3_partners_only setting}
G -->|on and not S3| R6[not_s3_partner]
G -->|pass| H[allowed - then role gate]
The gate is self-healing: when there is no cc_company row it calls
sync_roles_for_email() to re-pull the Moodle payload, and failing that
inserts a minimal stub row (LMS.php:1097). Pass $allow_writes = false
to get pure diagnostics with no side effects — that’s what the admin
lookup screens do.
Gate reasons map to user-facing messages via
carbon_company_gate_login_error_param() (LMS.php:414), which produces
a ?login_error= / ?cc_access_error= code rendered by
template-log-in.php:31 and carboncalculator_get_index_access_error_message()
(carboncalculator/functions.php:749).
CRN validation lives in UkCompanyCrn::normalize() / ::is_valid()
(includes/UkCompanyCrn.php). Normalisation strips non-alphanumerics and
uppercases; validation rejects placeholders such as all-zeros.
Role gate. Roles are Moodle shortnames synced into cc_user_roles
on login. The matrix lives in RolePermissions (includes/RolePermissions.php):
| Matrix column | Moodle shortnames |
|---|---|
| Default user (view only) | tlactionplans_company_member, scs_cc_viewer |
| Carbon Reporter | scs_cc_reporter |
| Partner Carbon Admin | scs_cc_admin |
| School Admin | tlactionplans_company_admin, tlactionplans_company_owner, tlactionplans_school_admin |
| Partner admin (supply-chain view bypass) | tlactionplans_partner_admin |
Access allowlist = Reporter + Partner Carbon Admin + School Admin +
Partner admin + tlactionplans_partner_owner + tlactionplans_partner_member
(RolePermissions.php:217). Viewer allowlist = the Default user roles.
Two counter-intuitive rules, both deliberate:
- A user with zero synced roles is allowed in as viewer-only, not
blocked (
user_has_calculator_role_access()returnstruefor an empty array,RolePermissions.php:520; the comment atLMSClient.php:1285confirms this was a deliberate change). scs_cc_admin(Partner Carbon Admin) is view-only on My Company Emissions even ifscs_cc_reporteris also assigned —user_can_edit_company_reports()returnsfalsefor that role before it checks reporter (RolePermissions.php:285).
Partner flags. cc_company.is_partner is the CC Partner flag, synced
from Moodle scs_is_cc_partner (with legacy fallback scs_is_partner).
The S3 Partner flag is cc_s3_partner inside cc_company.profile_json,
with a live Moodle-CRM fallback by CRN cached for an hour in the transient
cc_s3_partner_crm_{moodle_company_id} (LMS.php:325).
Configuration
Section titled “Configuration”Settings live on the Carbon subsite’s options table (read through
FeatureSettings, which switches blogs — includes/FeatureSettings.php:166):
| Option | Effect |
|---|---|
carboncalc_feature_partners_only_access | Require cc_company.is_partner |
carboncalc_feature_s3_partners_only_access | Require cc_s3_partner |
carboncalc_feature_sharing_enabled | Enables report sharing |
carboncalc_feature_debug_mode | Verbose debug UI |
carboncalc_feature_sic_lookup_indicator | Shows SIC lookup provenance |
carboncalc_block_users_without_crn (AdminSettingsPage::OPT_BLOCK_USERS_WITHOUT_CRN) | The CRN gate; defaults to ON |
Constants: CARBONCALC_DEV_BYPASS short-circuits the whole gate and
returns company id 1 (LMS.php:68). CARBONCALC_SKIP_AUTH disables
front-end auth checks in the theme. Both are local-only.
Invariants and gotchas
Section titled “Invariants and gotchas”- The CRN gate fails open only if the option row exists and is falsy.
is_block_users_without_crn_enabled()(FeatureSettings.php:65) returnstrueunless a row exists on the Carbon blog or blog 1 that reads'0'/false. A missing option means blocking is ON. - The setting is read from two blogs. Legacy saves landed on blog 1.
clear_legacy_crn_block_option_copies()(FeatureSettings.php:206) exists to clean those up. If the gate behaves inconsistently between wp-admin and the front end, check for a stale copy on blog 1. must_have_company()returnstruewhen the LMS user id is 0 but a company id resolved (Rest.php:214-217). A session with a company but no resolvable LMS id skips the gate for REST.- Two REST routes are deliberately public (
permission_callback => '__return_true'):/stepsand/fields(Rest.php:106,:130). Those return configuration, not company data. - The theme’s REST routes have no permission callbacks at all — every
route in
carboncalculator_rest_api_init()(carboncalculator/functions.php:381) uses__return_trueand checks auth inside the callback. If you add a route there, you must callcarboncalculator_require_supply_chain_edit_permission()or equivalent yourself; nothing does it for you. - The gate writes to the database (stub company rows, role sync) as a side
effect of a read-looking call. Pass
$allow_writes = falsefrom anything diagnostic. carbon_login_blocked_no_permitted_carbon_role()returnsfalsewhen the user has no roles at all — checkcarbon_login_blocked_no_synced_roles()separately if you need that distinction.
Changing it safely
Section titled “Changing it safely”- New gate conditions go in
carbon_company_access_gate_detail(), with a newreasonstring, acaseincarbon_company_gate_login_error_param(), a message intemplate-log-in.php, and a row incarbon_company_gate_steps()(LMS.php:652) so the admin diagnostics screen still explains the outcome. - New roles: add the shortname to the right constant in
RolePermissions, not to a call site. Every allowlist is filterable (carboncalc_calculator_access_role_allowlist,carboncalc_report_editor_roles,carboncalc_supply_chain_editor_roles,carboncalc_supply_chain_view_roles) — prefer the filter for environment-specific overrides. - Also update
get_matrix_rows()(RolePermissions.php:134) — that table is the client-facing spec rendered in wp-admin, and it is maintained by hand alongside the runtime checks. The two can drift. - Verify with wp-admin → Carbon Calculator → User Lookup: it renders the live gate steps, the Moodle payload hints, and the effective permission table for any user, without writes.
- Denials land in
wp-content/carboncalc-login-denied.logas one JSON object per line. Read it with the Carbon access log admin screen orLoginAudit::read_tail().
None. RolePermissions is pure enough to unit test (arrays in, booleans
out) and would be the highest-value place to start.
Related: [[moodle-sso-and-sessions]], [[company-lookup-integrations]], [[carbon-admin-and-debug-tools]].