4.8 Identity & Access
Every action in our systems is done by someone, and must be allowed. Two words carry this page:
- An identity is who acts: a person or a service.
- A permission is an action that identity may perform, such as reading a bucket or deploying a service. Permissions are grouped into roles.
Google Cloud checks permissions for everything that runs on it; our job is to define the identities and grant each one the roles it needs, and nothing more. Customers are the exception: their permissions live in our own data model and our systems check them.
4.8.1 People¶
Human cloud access is assigned through dedicated Google Workspace or Cloud Identity groups. Terraform manages the roles granted to each group on cloud resources; authorized directory administrators manage the people in those groups outside Terraform. Joining or leaving an existing group does not require a Terraform deployment.
Shared/stage access — dated evidence, reconciled 7 November 2026
On 5 November Terraform applied impersonation bindings for infra-admins on foundation, plan-stage and apply-stage, plus scoped observer bindings on the four stage projects. On 6 November Jacopo and Lorenzo were added as members of stage-release-admins; the Access delivery configuration grants that group approval of its stage rollout. The earlier statement that both stage groups were empty describes only the 5 November read. Production and PAM remain inactive; directory restrictions and access through groups without alternative personal grants still require their own checks. See the baseline register, which does not claim a new directory read.
| Group email | Purpose | Boundary and current status |
|---|---|---|
infra-admins@moonitoring.com |
Trusted infrastructure administrators across environments | Impersonate sa-tf-foundation and the plan/apply identities of active environments. Shared/stage initially; production only after explicit activation, within the managed-project boundary. No automatic organization-policy grant. |
stage-release-admins@moonitoring.com |
Stage release approvers | Approve application stage rollouts separately from PR review; the Access pipeline grant is conditioned on access-stage. Terraform build approval is a separate activation. No Terraform impersonation through this group. |
release-admins@moonitoring.com |
Production release approvers | Reserved for production; no live IAM grant or approval workflow. |
stage-observers-admins@moonitoring.com |
Inspect stage resources and operations | Direct, scoped metadata, ordinary-log and monitoring reads in the four stage projects. No Terraform impersonation/state, object contents, secret values, SQL login/query/export, API invocation or resource mutation. |
observers-admins@moonitoring.com |
Production observers | Reserved for the corresponding production observation scope; no live IAM grant. |
stage-infra-admins@moonitoring.com |
Unused existing group | No grants planned. Keep it unchanged; the single infra-admins group replaces a separate stage administrator group. |
Jacopo and Lorenzo are the agreed responsible people for these groups. The 5 November directory read confirmed both as OWNER and MEMBER of infra-admins; the 6 November read confirmed both as MEMBER of stage-release-admins. The last cited observer read, on 5 November, showed no members. stage-infra-admins is unused. The production approver's actual email remains release-admins@moonitoring.com: a lookup of prod-release-admins@moonitoring.com found no group, so a naming proposal must not be used as an IAM principal. The historical ownership and membership observations are recorded in the infrastructure access register. Settings that restrict membership changes and prohibit external members still require verification. Directory ownership, group membership and effective cloud permissions are separate checks.
The observer's SQL role is a narrow custom metadata role because the predefined Cloud SQL Viewer includes backup export permission. Ordinary logs and service configuration can contain application-written data; observer access does not redact that content. Studio and interactive SQL access are separate grants and are not part of this rollout. The infrastructure team-access runbook defines the resource grants, rollout checks and current status.
For each group, name an owner and a backup, record approved membership changes, and follow onboarding and offboarding. Directory membership does not synchronize application roles, capabilities, Organization memberships or entitlements.
Organization break-glass access, developer sandboxes, ML access and security-audit access require their own scoped decisions; the groups above do not grant them automatically.
Resource and IAM changes use reviewed Terraform plans. The stage infrastructure uses the reviewed manual plan/apply path; the application release path has its own approved Cloud Build and Cloud Deploy stages. Neither is enabled automatically by merging the documentation. Cloud Build's approver role is project-wide in the shared CI project, so different group names alone do not enforce separate stage/prod approval boundaries. The Access Cloud Deploy approver grant instead uses a pipeline binding with a target condition. Production approval activation remains blocked until all relevant scopes are resolved. See Infrastructure as Code.
The single infrastructure administrator group has broad write access through impersonation and can operate outside a release pipeline. Membership overlap and those direct permissions mean review and release approval are process controls, not hard separation between author and approver or approval for every Google Cloud operation. Directory membership changes use the separate audited process above.
Access to Prod¶
The target production policy is no standing access to prod data. The following Privileged Access Manager workflow remains a production design requirement, not a claim that it is deployed:
| Grant | Gives | Lasts | Condition |
|---|---|---|---|
prod-db-access |
The Dev ORM of one system on that system's prod database, signing in with the person's own Google identity | 1 hour | A second person approves |
prod-debug |
Logs and viewer in prod | 4 hours | Granted at once, an alert is sent |
4.8.2 Services¶
The table below names the ten new identities created and verified on 5 November 2026 in stage phase 1. It is not a list of deployed workloads or SQL logins. The earlier four runtime identities and their SQL checks remain recorded in the 4 November stage acceptance record; that record does not certify access for the new identities or databases.
Every service and job runs as its own service account, named sa-<role>, with only the roles its work needs. An API and its migration job have separate identities; future workers also receive their own. Service accounts have no keys: a service gets short-lived tokens from Google Cloud, and pipelines borrow an identity through impersonation. Where SQL access is enabled, services authenticate through IAM; phase 1 grants no new SQL login or access.
| System | Runtime project ID | Initial API identity | Migration identity |
|---|---|---|---|
access |
moonitoring-platform-stage |
sa-accounts |
sa-access-migrate |
releases |
moonitoring-platform-stage |
sa-release |
sa-releases-migrate |
ai-gateway |
moonitoring-platform-stage |
sa-gateway |
sa-ai-gateway-migrate |
farm |
moonitoring-farm-stage |
sa-farm-api |
sa-farm-migrate |
moochat |
moonitoring-farm-stage |
sa-moochat |
sa-moochat-migrate |
status |
moonitoring-platform-stage |
sa-status |
sa-status-migrate |
news |
moonitoring-platform-stage |
sa-news |
sa-news-migrate |
Every other component of a system has its own identity, sa-<system>-<component> (e.g. sa-releases-consumer, sa-releases-relay, sa-releases-sweeper, sa-access-sweeper); the API and migration names of the table stay as they are (decided 2026-11-11). Every push subscription and every Cloud Scheduler call has an identity of its own, which infra creates with the link and which may invoke only its target: sa-ps-<key> for a push, sa-sch-<key> for a schedule, the key at most 23 characters (e.g. sa-sch-acc-relay, sa-ps-acc-audit-farm); the receiver admits exactly those identities and checks as audience its fixed URL https://<service>-<project number>.europe-west8.run.app, and a push subscription lives in the consumer's project (decided 2026-11-11).
Apps' servers have their own identities, with no database (decided 2026-11-11): sa-console (Console server, delivery console), sa-account (Account app, account) and sa-dashboard (Organization Dashboard server, dashboard), each in moonitoring-platform-stage and allowed to invoke only the backends it calls. An app's secrets are named <app>-<purpose>, e.g. account-session-password, dashboard-session-key.
These are the selected names for the new per-component identities, not names inferred from the legacy Terraform inventory. The API names sa-accounts, sa-release and sa-gateway retain the earlier target catalog's spelling; sa-moochat and the sa-<system>-migrate names complete this phase's selection. Their databases are hosted in moonitoring-db-stage, not in the projects holding these identities. The project display-name alignment preserves these project IDs and service-account identities.
The existing four legacy runtime identities and the platform database were preserved by phase 1. That provisioning phase alone activated no SQL IAM users, Cloud SQL Client/Instance User grants, PostgreSQL privileges or workload images for the identities above. Access enablement followed separately; the 6 November SQL checks distinguish runtime from migrator and test cross-database denials. Those early checks preceded the application migration and do not alone certify later table privileges. A service-account name confers no database authority.
The follow-up on 5 November provisioned the news database in moonitoring-db-stage and the two identities below. The subsequent Terraform refresh/plan shows no differences; this is a provisioning result, not SQL-access or application acceptance:
| System | Runtime project ID | API identity | Migration identity | Status |
|---|---|---|---|---|
news |
moonitoring-platform-stage |
sa-news |
sa-news-migrate |
Provisioned; no workload or verified SQL access |
Provisioning a database or identity and verifying SQL access are separate milestones. The first SQL baseline is limited to Access, with positive and negative privilege checks before workload activation. The other new system identities, including News, remain dormant; they receive no SQL access through this initial Access enablement.
Console, further workers (including ingestion, parser, resolver and export components) and Dev ORM identities remain separate future deployment work. The static site of news is hosted on Cloudflare; its Google Cloud backend is separate future deployment work, as shown in Where the Systems Run Today. The ML target identities (sa-ml-pipelines, sa-ml-endpoints) and the shared Terraform/build identities remain outside this phase; see Infrastructure as Code for the latter.
Services call their destinations directly using their own Google identity and scoped IAM invocation grants. Access is not a proxy for every internal request. The destination also applies its application policy: which RPCs a caller may use, which replica it may receive, and which user context is required. Being on the subnet or listed among known service accounts does not grant every operation: a service's list of callers is authentication only, and every RPC checks the caller for its own operation (decided 2026-11-11, gRPC Between Services). The Console's server verifies IAP before using a staff token; see the staff ingress contract. Directory access to the Console, staff capabilities in Access, and SQL privileges are separate controls.
4.8.3 Customers¶
This is the target application authorization flow. Creating cloud access groups does not implement it.
Organization users sign in through access, which uses Identity Platform underneath and signs the short-lived token that every other system verifies. What a user may do is decided in two steps, both checked by the system that receives the request:
- The user's role in the Organization the request concerns must include the capability, for example
farm.models.use. A direct role counts when that Organization is the token's active Organization; a delegated role counts only when the token's active Organization is the delegation's delegatee, and the two are never added together (Choosing the Organization). - The Organization must hold the entitlement, for example
farm.models.mastitis. Through a delegation, the entitlement checked is the acting Organization's, the token's active Organization (decided 2026-11-11): an insurer or a cooperative pays for what it uses on the farms delegated to it, and the farm's own plan neither limits nor pays for that use.
The data behind both is described in Organizations & Access.
A customer signs in on the sign-in page of the Account app (decided 2026-11-10), with e-mail and password or Continue with Google. The page also offers Forgot password and Create an account; Identity Platform handles all four, sends the reset e-mail and verifies the e-mail of a new account before it can be used, so access never sees or keeps a password. A person who signs in for the first time then registers as the Tokens API describes: name, surname and telephone, and the language of the apps and e-mails (RegistrationRequest.locale).
Signing In from an App¶
Every app signs a person in through the Account app, with the same flow (decided 2026-11-10 for native apps, 2026-11-11 for web apps). The app never shows a password field and never sees the password; the sign-in page is the Account app's, and access issues the tokens as for every other sign-in. This is the OAuth 2.0 authorization-code flow with PKCE (RFC 7636), for native apps as RFC 8252 describes it, with access as its only server. Only the Account app exchanges an Identity Platform sign-in itself; every other app gets its tokens with a code.
A native app, such as the farm sync client or its installer:
- The app listens on a random port of
127.0.0.1, makes a PKCE verifier and astate, and opens the system browser ataccount.moonitoring.com/authorizewith the parameters of RFC 6749 and 7636,response_type=code,client_id,redirect_uri,code_challenge,code_challenge_method=S256andstate, plusdevice_name(the computer's name) and, when a fresh sign-in is needed,prompt=login. - The person signs in there (Identity Platform behind
access, as on the web) and sees which app on which computer asks to use their account, with Allow and It's not me. - On Allow, the Account app asks
accessfor a one-time code bound to the app, the loopback address and the challenge (POST /tokens/v1/authorizations) and sends the browser back to the app's loopback address with the code and thestate. The page then says the person can go back to the app. - The app checks the
stateand exchanges the code and its verifier atPOST /tokens/v1/tokens(authorization_codegrant) for the person's tokens. Its/callbackanswers the browser with a redirect toaccount.moonitoring.com/authorize/done?client_id=…, the page that says the person can go back to the app, as Claude Code does; the loopback itself shows nothing. - The token has no active Organization when the person belongs to more than one (decided 2026-11-10), for this grant and for the identity-platform grant alike: the app asks which one, as Choosing the Organization describes, and a refresh with
organization_idswitches to it. With one Organization the token carries it.
A web app, such as the Organization Dashboard, signs in the same way through its own server (decided 2026-11-11), the backend for frontend, as the Console's server is for staff (Staff ingress contract):
- A person who opens the app without a session is sent by the app's server to
account.moonitoring.com/authorizewithresponse_type=code, the app'sclient_id, itsredirect_uri, an S256code_challengeand astate. The server keeps the verifier, thestateand the page that was asked for in a short-lived HttpOnly cookie of its own host. - The person signs in on the Account app, or is already signed in there, and goes straight back: our own web apps skip the Allow page, as Google's own apps do, because they are first-party and ask for nothing more than the sign-in itself. The Account app asks
accessfor the code (POST /tokens/v1/authorizations) and sends the browser to the app's/auth/callbackwith the code and thestate. - The server checks the
state, exchanges the code and its verifier atPOST /tokens/v1/tokens, and keeps the access and refresh tokens in its own session: a cookie of its host that is HttpOnly, Secure and SameSite=Lax, sealed with a key of the app's server, so any instance can serve it. Then it sends the browser to the page that was asked for. - The page never sees a token. It calls
/api/<system>/on its own host; the server forwards each call with the access token inAuthorizationand renews it with the refresh token before it expires. A call that changes something is accepted only from the app's own origin. - Switching Organization is a refresh with
organization_id, made by the server, as for a native app; the person chooses as Choosing the Organization describes. -
Sign-out revokes the refresh token (
POST /tokens/v1/tokens/revoke) and ends the session; the browser then signs out of the Account app too, so the next sign-in on that computer asks for the password. -
Identity Platform proves the password;
accessdecides who the person is in MooNitoring and what they may do. We never keep passwords. Locally, the Firebase Auth emulator stands in for Identity Platform, andaccessaccepts its tokens only withMOONITORING_ENVIRONMENT=local. -
Only registered apps can ask, each with a fixed client id and its own redirects; any other redirect is refused, compared exactly, so a code can only reach the app that asked:
- the native apps, the farm sync client
moonitoring-farm-syncand the installer ofreleases,moonitoring-installer(decided 2026-11-10): the redirect must behttp://127.0.0.1:<port>/callback, so a code can only reach the computer where the sign-in started; - the web apps, today the Organization Dashboard
moonitoring-dashboardand later MooGrant (decided 2026-11-11), each with an exact allowlist per environment:https://dash.moonitoring.com/auth/callbackin prod,https://stage-dash.moonitoring.com/auth/callbackin stage, andhttp://localhost:9200/auth/callbackonly withMOONITORING_ENVIRONMENT=local.
The code lasts 60 seconds, works once, and only with the verifier of its challenge, for the client and the redirect it was issued for. - A refresh token belongs to one app and never reaches a page: a native app keeps it only in the operating system's credential store of that Windows user, a web app only in its server's session, and each revokes it at sign-out. - A write that needs an admin capability asks for a sign-in at most five minutes old; a read never does, even under an admin capability (decided 2026-11-11). The token carries the time of the sign-in on the Account app; when it is older, the system answers 403
reauthentication-requiredand the app sends the person back to the Account app withprompt=login, which asks for the password again: a native app in the browser, a web app with a redirect that comes back to the same page. - The installer signs the person in once for both apps (decided 2026-11-10): with the person's token it installs the client and, when the person chooses a farm, connects the computer and leaves a hand-over bundle, so the sync client starts already connected and does not ask the person to sign in (Installer and Client Library). - A native app waits for the browser at most ten minutes (decided 2026-11-10). Then it stops listening on its loopback port, so a late answer reaches nobody, and offers to start again. - "It's not me" ends the attempt: the Account app sends the browser back to the app's loopback address witherror=access_deniedand the samestate, as OAuth 2.0 does; the app checks thestate, stops, and says that nobody was connected. No code is issued. - Allow needs a recent sign-in (decided 2026-11-10): when the person signed in to the Account app more than five minutes before, the Allow page of a native app asks for their password again (Identity Platform reauthentication) before it callsPOST /tokens/v1/authorizations, so a browser left signed in on a shared computer cannot connect a new app. A web app has no Allow page, soaccessasks for this recent sign-in for native apps only. - In stage the person signs in twice: with a Google account for Identity-Aware Proxy (Network), then with their MooNitoring account. Prod has only the second. - the native apps, the farm sync client
Choosing the Organization¶
After signing in, every app asks which Organization the person works in, when they belong to more than one (decided 2026-11-11): the native apps, the web apps and MooChat alike. Who the person acts as is separate from which farm they look at.
- The choice offers only the person's own Organizations: those where they hold a direct membership or a role through a user group, the
membershipsofGET /organizations/v1/me, each with its name, type and status. A delegated membership is never an identity to choose. - The chosen Organization becomes the token's active Organization, with a refresh that names it (
organization_id), and each app remembers the last choice and offers it first the next time. With one Organization nothing is asked. - Its type tells the app what to show: a Farm its own herd, a Cooperative, an Insurer or a Professional the farms that share data with it. MooChat, for example, offers a farmer's tools to a Farm and an insurer's to an Insurer; the design of MooChat comes later.
- Acting as a delegatee, the person picks the subject of their work. The app lists the farms that delegated to the active Organization, with the delegated role (
delegated_organizationsofGET /me), and the person picks one; the app then calls that farm's resources with its id in the path. The token stays the delegatee's, and picking another farm changes no token. - Every system applies the same rule to its copy of memberships: a delegated membership counts only when the token's active Organization is the delegation's delegatee. Acting as their own farm a person never uses delegated rights, and acting as an insurer never the rights they hold in their own farm; rights from the two are never mixed.
- The audit names all three: the person, the Organization they acted as and the subject, such as "Mario Rossi, for Assicurazioni X, on Cascina Uno" (PlatformAuditEvent).
4.8.4 Audit¶
The following is the target centralized audit design, not a claim that the sink, locked retention, pgAudit or application audit pipeline has been deployed. Group-membership changes must still have their own directory audit evidence.
Every change leaves a record that the person who made it cannot erase.
- An organization log sink copies the audit logs of every project into a bucket in
moonitoring-loggingwhose retention is locked for 400 days: not even an administrator can delete them earlier. - Writes made through the Console or the admin APIs are also recorded as
PlatformAuditEventrows: who, what, when and which capability was used. Each system publishes its rows on its own topic,<system>.audit.v1, in its own project (decided 2026-11-11): only that system's backend and relay hold the publisher role on it, andaccess-consumerhas one subscription on each system's topic, so the system is known from the subscription. Pub/Sub carries no identity of the publisher, so a topic shared by every system could not prove where an event came from. A Cloud Storage subscription on each audit topic copies the events to the locked bucket. - On prod, Cloud SQL's pgAudit records changes to tables, roles and permissions, and every statement run through the Dev ORM, in the same logs.
- Data Access logs are enabled only where they matter (secrets, the raw bucket, the Dev ORM), because they are billed by volume.