AGM

AGM (Admin UAA) is the user realm for the ShopSTAR3 admin plane. It owns all staff principal records, role grants, access restrictions, and external federation source configuration. It is the source of truth for who admin users are and what they are permitted to do — not for how they prove their identity.

The identity-service consumes AGM’s gRPC read API to resolve principals during authentication. Identity-service owns JWT issuance, JWT validation, and all external federation protocol flows (SAML, OAuth, LDAP). AGM does not issue or validate tokens.

flowchart LR
    SPA([Admin SPA]) -->|REST management API| AGM[AGM]
    IS[identity-service] -->|gRPC — resolve principal\nfetch restrictions\nMFA secret| AGM
    AGM -->|store.provisioned\nstore.suspended| KF([Kafka])
    KF --> SVC[all services]

Boundary#

ConcernOwner
Staff principal recordsAGM
Role grantsAGM
Access restriction rulesAGM
Federation source configurationAGM
Store master dataAGM
JWT issuance and validationidentity-service
SAML / OAuth / LDAP protocol flowsidentity-service
Session and refresh token lifecycleidentity-service

Principal Model#

AGM manages staff principals only. Customer identities are held by customer-service (profiles) and identity-service (auth).

CREATE TABLE principal (
  id            UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
  email         VARCHAR(255) NOT NULL UNIQUE,
  display_name  VARCHAR(255),
  status        VARCHAR(16)  NOT NULL DEFAULT 'ACTIVE'
                  CHECK (status IN ('ACTIVE', 'SUSPENDED', 'OFFBOARDED')),
  mfa_enabled   BOOLEAN      NOT NULL DEFAULT TRUE,
  mfa_method    VARCHAR(8)   NOT NULL DEFAULT 'TOTP'
                  CHECK (mfa_method IN ('TOTP', 'EMAIL')),
  mfa_secret    TEXT,                          -- TOTP secret, encrypted at rest; NULL for EMAIL method
  source        VARCHAR(16)  NOT NULL DEFAULT 'LOCAL'
                  CHECK (source IN ('LOCAL', 'FEDERATED')),
  federation_source_id UUID  REFERENCES federation_source(id),
  created_at    TIMESTAMPTZ  NOT NULL DEFAULT now(),
  updated_at    TIMESTAMPTZ  NOT NULL DEFAULT now()
);

-- In-DB credentials (LOCAL principals only)
CREATE TABLE credential (
  principal_id  UUID PRIMARY KEY REFERENCES principal(id) ON DELETE CASCADE,
  password_hash TEXT    NOT NULL,              -- bcrypt
  must_reset    BOOLEAN NOT NULL DEFAULT FALSE,
  last_changed  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Store-scoped role grants
CREATE TABLE role_grant (
  id           UUID        PRIMARY KEY DEFAULT gen_random_uuid(),
  principal_id UUID        NOT NULL REFERENCES principal(id) ON DELETE CASCADE,
  store_id     UUID        NOT NULL,
  role         VARCHAR(64) NOT NULL,
  granted_by   UUID        REFERENCES principal(id),
  granted_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (principal_id, store_id, role)
);

Role grants are store-scoped. A staff member may hold different roles across different stores simultaneously — ADMIN on Store A and CATALOG_EDITOR on Store B. There are no global roles that implicitly span stores.

Access Restrictions#

Restriction rules are stored per principal or per role, optionally scoped to a specific store. The identity-service reads and enforces them at login time.

CREATE TABLE access_restriction (
  id           UUID        PRIMARY KEY DEFAULT gen_random_uuid(),
  subject_type VARCHAR(16) NOT NULL CHECK (subject_type IN ('PRINCIPAL', 'ROLE')),
  subject_id   VARCHAR(128) NOT NULL,          -- principal UUID or role name
  store_id     UUID,                           -- NULL = platform-wide
  type         VARCHAR(32) NOT NULL,
  config       JSONB       NOT NULL,
  active       BOOLEAN     NOT NULL DEFAULT TRUE,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);
Restriction typeConfig shape
IP_ALLOWLIST{ "ranges": ["203.0.113.0/24"] }
TIME_WINDOW{ "timezone": "America/Chicago", "windows": [{ "days": ["MON",...], "start": "08:00", "end": "18:00" }] }
STORE_SCOPE{ "allowed_store_ids": ["uuid-1", "uuid-2"] }
MFA_REQUIRED{ "enforce": true }

Federation Sources#

AGM stores the connection parameters for each external identity provider. The identity-service uses these to execute the actual SSO protocol flows. Secrets (OIDC client secrets, LDAP bind passwords) are stored as Vault path references — AGM never holds them in plaintext.

CREATE TABLE federation_source (
  id         UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
  name       VARCHAR(128) NOT NULL,
  type       VARCHAR(8)   NOT NULL CHECK (type IN ('SAML', 'OIDC', 'LDAP')),
  store_id   UUID,                             -- NULL = platform-wide
  config     JSONB        NOT NULL,
  active     BOOLEAN      NOT NULL DEFAULT TRUE,
  created_at TIMESTAMPTZ  NOT NULL DEFAULT now()
);

SAML config shape#

{
  "idp_metadata_url": "https://idp.example.com/metadata.xml",
  "sp_entity_id": "https://agm.ss3.internal/saml/sp",
  "attribute_mapping": {
    "email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
    "display_name": "http://schemas.microsoft.com/identity/claims/displayname"
  },
  "sign_requests": true,
  "want_assertions_signed": true
}

OIDC config shape#

{
  "discovery_url": "https://accounts.google.com/.well-known/openid-configuration",
  "client_id": "...",
  "client_secret_ref": "ss3/kv/agm/oidc/{source_id}/client_secret",
  "scopes": ["openid", "email", "profile"],
  "attribute_mapping": { "email": "email", "display_name": "name" }
}

LDAP config shape#

{
  "host": "ldap.corp.example.com",
  "port": 636,
  "use_tls": true,
  "bind_dn": "cn=agm-svc,dc=example,dc=com",
  "bind_password_ref": "ss3/kv/agm/ldap/{source_id}/bind_password",
  "user_search_base": "ou=users,dc=example,dc=com",
  "user_search_filter": "(mail={email})",
  "attribute_mapping": { "email": "mail", "display_name": "cn" }
}

Identity-Service Integration#

The identity-service calls AGM via gRPC to resolve principals and fetch restrictions during authentication. AGM does not call the identity-service.

service AgmReaderService {
  rpc ResolvePrincipal(ResolvePrincipalRequest) returns (PrincipalResponse);
  rpc ResolveOrProvisionFederated(FederatedIdentityRequest) returns (PrincipalResponse);
  rpc GetRestrictions(GetRestrictionsRequest) returns (RestrictionsResponse);
  // Returns method (TOTP|EMAIL) and secret for TOTP; triggers email OTP dispatch for EMAIL
  rpc GetMfaChallenge(GetMfaChallengeRequest) returns (MfaChallengeResponse);
  rpc GetFederationSource(GetFederationSourceRequest) returns (FederationSourceResponse);
}

PrincipalResponse carries id, status, mfa_enabled, source, and the full set of store-scoped role grants. The identity-service uses the role grants to populate the roles claim in the platform JWT.

JIT Provisioning#

When a federated login arrives for an unknown email address, the identity-service calls ResolveOrProvisionFederated. AGM creates a principal record (source = FEDERATED) with no role grants and returns it to the identity-service to continue the auth flow. An admin must grant roles before the new user can access any store.

Management API#

AGM exposes a REST management API consumed by the admin SPA. All routes require a platform-level staff principal.

MethodRouteDescription
GET/admin/principalsList staff principals
POST/admin/principalsCreate staff principal
GET/admin/principals/{id}Get principal
PATCH/admin/principals/{id}Update profile or status
DELETE/admin/principals/{id}Offboard (soft-delete)
POST/admin/principals/{id}/reset-passwordForce password reset
GET/admin/principals/{id}/rolesList role grants
POST/admin/principals/{id}/rolesGrant role on a store
DELETE/admin/principals/{id}/roles/{grantId}Revoke role grant
GET/admin/principals/{id}/restrictionsList restrictions
POST/admin/principals/{id}/restrictionsAdd restriction rule
DELETE/admin/principals/{id}/restrictions/{restrictionId}Remove restriction
GET/admin/federation-sourcesList federation sources
POST/admin/federation-sourcesCreate federation source
PATCH/admin/federation-sources/{id}Update source config
DELETE/admin/federation-sources/{id}Deactivate source

Kafka Events#

AGM consumes store.provisioned from store-service to register new store UUIDs as valid targets for role grants and restrictions. AGM does not publish store lifecycle events.

TopicDirectionPurpose
store.provisionedConsumed from store-serviceRegister store UUID as a valid role grant target

Vault Configuration#

Key pathPurpose
ss3/kv/agm/db.urlPostgreSQL JDBC URL
ss3/kv/agm/db.usernameDatabase username
ss3/kv/agm/db.passwordDatabase password
ss3/kv/agm/mfa.encryption_keyAES key for TOTP secret encryption at rest
ss3/kv/agm/oidc/{source_id}/client_secretOIDC client secret per federation source
ss3/kv/agm/ldap/{source_id}/bind_passwordLDAP bind password per federation source
ss3/kv/shared/Shared Kafka bootstrap and OTel config