Manage read-only accounts¶
Use an account-wide read-only restriction when a Principal must keep its ordinary catalog visibility but must not perform user-initiated mutations. The restriction overrides owner-Group membership, Purge Grants, staff status, and superuser status. It grants no new read permission.
This restriction is separate from an entity being managed by
catalog-info.yaml: the account flag limits what a Principal may do, while
YAML provenance controls how a particular entity is managed.
Prerequisites and boundary¶
- Run the database migrations that create
AccountAccessand its audit trail. - To change the flag in Django administration, use a non-read-only operator
who can change both the target User and
AccountAccess. - Use the infrastructure recovery command only from an initialized backend environment with controlled shell access.
Atlas enforces the current persisted flag at authorization boundaries. The UI
also hides write controls, but it is not the security boundary. A missing
AccountAccess row means unrestricted by this feature; a database error while
checking the flag fails closed.
Set or clear the flag in Django administration¶
- Sign in to
/admin/as an authorized, non-read-only operator. - Open Users, then open the exact target User.
- In Account access, set Read only to the intended value and save the User form.
- Reopen the target User and verify the displayed value.
- Inspect the corresponding
AccountAccessAuditRecordthrough the approved database/audit tooling. It records the operator, target id and username, old and new values, action, and timestamp.
The User change and side-car audit record commit in one transaction. The inline cannot delete the side-car row: clear the checkbox to restore ordinary authorization. A read-only administrator cannot change any admin-managed state or clear its own restriction, even with a crafted request.
What the affected user observes¶
GET /api/me/ reports the current state as isReadOnly. The frontend refreshes
it after login, when the page regains focus or visibility, on entry to a write
route, and after a denied write. Existing browser sessions do not need to log
in again.
The next authorization check after setting the flag denies user-initiated catalog, relationship, tag, settings, access-control, admin, and installed plugin mutations before their side effects. Clearing the flag restores the normal evaluator decision; it does not grant a write that the account could not otherwise perform.
Login, logout, session maintenance, security audit, Core-controlled identity
or profile provisioning, supported credential-security flows, and independently
scheduled ingestion keep their existing service policies. They cannot change
AccountAccess, and a caller cannot claim a service/source identity to bypass
the restriction.
An operation already authorized and in flight when the flag commits is not retroactively cancelled. Previously committed work is not undone. Future user-triggered background jobs must retain the initiating Principal and check the current flag again before mutation; the current distribution has no such user-triggered job dispatcher.
Recover when every writable operator is locked out¶
clear_read_only is an infrastructure-only escape hatch. It is a dry run by
default and requires the exact username and a non-blank audit reason. From
core/backend, preview the change first:
uv run python manage.py clear_read_only exact-username \
--reason "approved incident or ticket reference"
Confirm that the output names the expected username, numeric id, current and proposed values, and reason. Apply the same exact operation only after that review:
uv run python manage.py clear_read_only exact-username \
--reason "approved incident or ticket reference" \
--confirm
The confirmed change is transactional and creates an audit record whose
operator is empty and whose reason is prefixed with infrastructure recovery.
There is no equivalent browser or public API operation. Use --set true only
when an approved infrastructure procedure deliberately sets, rather than
clears, the restriction.
External accounts that must be read-only on first access¶
Atlas supports first-access read-only issuance through preprovisioning. Follow this order; allowing automatic provisioning before the flag and exact link exist creates a writable window and is not equivalent:
- Atomically create the Principal with
read_only=truethrough the operator workflow. - Preview and then create the exact identity link. Use identifiers from the trusted provider administration surface, never an email or display name:
uv run python manage.py manage_auth_identity link \
--provider atlas.auth.oidc \
--source https://idp.example \
--subject 248289761001 \
--principal-id 42 \
--reason "approved read-only external access"
uv run python manage.py manage_auth_identity link \
--provider atlas.auth.oidc \
--source https://idp.example \
--subject 248289761001 \
--principal-id 42 \
--reason "approved read-only external access" \
--apply
- Configure that provider with
principalProvisioning: preprovisionedbefore permitting login. Do not useautomaticfor this issuance workflow. - Verify that a missing link is denied, then verify first-login read success and direct-write rejection.
Provider claims, profile updates, identity linking, manual/provider membership
grants, Purge Grants, and administrator break-glass preserve the side-car
unchanged and cannot override it. The identity-link command cannot set or clear
read_only; use the audited account operator workflow for that state.
Security-sensitive rollback¶
Once any account is flagged, deploying a version that ignores
AccountAccess silently restores its write privileges. Before rollback,
export and review the affected Principals and choose one of these controls:
- keep an equivalent mandatory write guard in the rollback version; or
- block/deactivate every affected account before the old code can serve authenticated requests.
Preserve the AccountAccess and audit data through forward and reverse
migrations. Do not drop the table, recreate rows with defaults, or describe a
schema rollback as harmless. After the deployment, test a flagged owner,
superuser, and Purge Grant holder against a direct write, not only the UI.
Verify the result¶
With the target account's existing session, confirm that a permitted catalog
read still behaves normally and a direct API write returns 403 with no
partial state or queued work. Then clear the flag and verify that the same
write returns to ordinary permission evaluation rather than becoming
unconditionally allowed.
See Permissions for the authorization model, Auth and identity for Principal/Actor separation, and Upgrade for the general schema rollback boundary.