Skip to content

Service Users

Service users are UserAccount records that represent machine-to-machine API access rather than a person signing in through the UI. They were introduced so an opaque API key can resolve to a first-class account in the Accounts domain. That gives API-key traffic the same authorization model, audit identity, and SaaS-admin flag behavior as other authenticated actors, without pretending that the caller is an interactive human user.

The core idea is deliberately narrow:

  • a service user is a UserAccount with isService = true;
  • the API key is never stored directly on the user account;
  • the configured SHA of the API key is stored in sha;
  • the service user’s email address is the stable configured identity for that API key;
  • service users are managed from application configuration, not from the UI.

The CHANGELOG.md entry summarizes this as: API keys are mapped to dedicated service users; those users cannot log in through the UI and can only access the API. This document expands that concept for maintainers and operators.

The service-user change adds two fields to UserAccount:

  • sha: Sha?
  • isService: Boolean

sha is the lookup key derived from the presented opaque token. isService marks the record as a service account rather than a person. These fields sit alongside existing account properties such as oidcSub, isSaasAdmin, identity, settings, subscription, and active agency.

The identity of a service user is intentionally stable. The email address, eId, and oidcSub of a UserAccount are immutable. This matters because these values are used as durable references in audit trails, actor resolution, and other domain records. If an API key needs to represent a different identity, the safe model is to configure a new service user or let the old one be pruned, not to mutate the core identity of the existing user.

Service users also receive normal account fields:

  • subscription is active when the service user is created by the system;
  • isSaasAdmin is controlled by configuration;
  • identity.email is controlled by configuration;
  • oidcSub is generated by the system when the service user is created.

No personal tenant or AgentFor record is created as part of automatic service-user creation. Service users exist to authenticate API access and resolve actor identity, not to onboard a person into a tenant workflow.

Service users are configured under the user-account module extras:

userAccount {
extras {
services {
"sha256_<hash>" {
emailAddress = "service-name@example.com"
isSaasAdmin = false
}
}
}
}

The map key is the SHA used to look up the service user. The value supplies the account identity and whether that service account should be treated as a SaaS admin.

Only configured service users should remain active. If configuration changes remove a service email, the pruning migration deletes the stale service-user account. This makes configuration the source of truth for the allowed set of machine users.

When a request is authenticated with an opaque token, the endpoint actor interceptor treats it as service authentication. The token is hashed, prefixed with sha256_, and used to build a ServiceActor. The actor is then enriched through UserAccountService.findUserBySha.

That lookup has four cases:

  1. The SHA is not configured. The request cannot be resolved to a service user and fails authentication.

  2. The SHA and configured email match an existing user. The existing user is returned after synchronizing mutable service-user attributes such as sha, isService, and isSaasAdmin.

  3. The configured email exists but the SHA is missing. The existing account is converted into a service user by setting service-specific mutable fields. Its immutable identity remains unchanged.

  4. No configured email exists. A new service user is created with the configured email, generated oidcSub, configured sha, configured isSaasAdmin, and active subscription.

The lookup intentionally includes the configured email when matching by SHA. This avoids accidentally reusing an old service user if a SHA is reassigned to a different configured email. The email is the configured identity boundary; the SHA is the credential lookup key.

Before this change, opaque-token authentication could only produce a generic service actor. With service users, it resolves to an actor that carries account-level details:

  • service SHA;
  • oidcSub;
  • userEid;
  • userEmail;
  • isSaasAdmin.

This means downstream authorization can reason about service callers using the same account-derived fields available for human users. The actor identity used for logs and authorization messages is still explicit: service actors identify as service <sha>, while user actors identify as user <subject>.

Service actors are not tenant actors. They do not receive tenant membership through AgentFor enrichment. Where service access is allowed, domain logic should handle ServiceActor deliberately. Where tenant membership is required, a service actor should not silently pass as a tenant user.

Service users are created lazily. The system does not pre-seed every configured service user at startup. Instead, the first authenticated request for a configured SHA resolves or creates the corresponding account.

Startup does perform cleanup. The ServiceUserPruningMigrationService runs as a startup migration and calls UserAccountService.pruneTechnicalUser(). Despite the legacy method name, the behavior is service-user pruning:

  1. list all UserAccount records with isService = true;
  2. build the configured set of service-user email addresses from servicesBySha;
  3. delete any service user whose email is no longer configured.

The migration records deletions in MigrationStats.deleted and logs a ServiceUserPruning migration summary. This makes startup cleanup consistent with the existing startup migration pattern used by AgentFor subject backfill.

Pruning by email rather than SHA is intentional. A key can rotate, which changes the SHA, but the service identity can remain the same. If the configured email is still present, the account remains valid and findUserBySha can synchronize the new SHA on the next lookup. If the email disappears from configuration, the service identity is no longer allowed and the old service user should be deleted.

To add a service user:

  1. choose a stable email-like identity for the service;
  2. configure the SHA of the opaque token under userAccount.extras.services;
  3. set isSaasAdmin only when the service must operate with SaaS-admin privileges;
  4. deploy the configuration.

To rotate an API key:

  1. update the configured SHA for the same email;
  2. deploy the configuration;
  3. allow the next request to synchronize the service user’s sha.

To remove a service user:

  1. remove its email entry from configuration;
  2. deploy the configuration;
  3. let startup pruning delete the stale service-user record.

Avoid changing a configured service user’s email to mean “same service, new address.” Email is treated as immutable account identity. If the identity really changes, configure the new email and let the old one be pruned.

  1. Helm’s rich function library makes it simple and safe to configure service users. Use Helm sha256sum function to define the service account in deployment secrets and keep it always synchronized with the actual API key.

Example:

userAccount.extras.services.sha256_{{ sha256sum "{{ .ArdaApiKey }}" }}.emailAddress=service+arda@arda.cards
userAccount.extras.services.sha256_{{ sha256sum "{{ .ArdaApiKey }}" }}.isSaasAdmin=true
  1. This implementation prefixes the SHA with sha256_ to enable other SHA algorithms in the future.

Service users are powerful because they translate API keys into first-class authenticated actors. Their configuration should therefore be handled like other security-sensitive deployment configuration:

  • store API keys in secret management, not in source;
  • configure only the SHA in Accounts;
  • grant isSaasAdmin sparingly;
  • use dedicated service-user emails that make ownership obvious;
  • remove unused service users from configuration rather than leaving dormant identities around.

Service users reduce ambiguity. A request authenticated by API key is no longer merely “some service”; it resolves to a specific configured account, with a durable account ID and subject for audit and authorization behavior.