Documentation
¶
Overview ¶
Package platformadmin implements the BACKEND of the admin panel (ADMIN-API-V1): a platform super-admin that lives ABOVE the tenants, plus a consolidated admin API for managing tenants, their users, and their observability.
The guiding principle is that the admin panel is NOT a second permission system. It INHERITS what already exists:
- auth-as-product (pkg/userauth): the super-admin authenticates with the same argon2id password + TOTP MFA mechanism a tenant user does — but against a SYSTEM schema (appximo_system.platform_admins), not a tenant. The reusable primitives are exported from pkg/userauth (see primitives.go).
- schema-per-tenant isolation (search_path / tenant_<id> identifiers): a tenant admin physically cannot reach another tenant; this is structural, not coded.
- the schema RBAC: "tenant admin" is just a tenant user whose role grants broad access — not a new concept.
- the control plane (pkg/controlplane): tenant create/lookup is WRAPPED, never reimplemented.
- the observability store (pkg/observability): per-tenant traces/metrics are EXPOSED with the right authorization, never duplicated.
This package adds only what did not exist: the platform-level super-admin (today the bare X-Admin-Key machine credential has no human identity) and the HTTP API that consolidates the above behind one authenticated, audited surface.
Index ¶
- Constants
- Variables
- type Admin
- type Config
- type ObsHandler
- type PlatformAuthResult
- type PlatformClaims
- type PublicAdmin
- type PublicTenantUser
- type ResourceField
- type ResourceHandler
- type ResourceInfo
- type Service
- func (s *Service) ActivateTenant(ctx context.Context, id string) error
- func (s *Service) ApplyTenantSchema(ctx context.Context, id string, sc *schema.APISchema, approved []string) (*migration.ApplyOutcome, error)
- func (s *Service) Bootstrap(ctx context.Context, email, password string) (PlatformAuthResult, error)
- func (s *Service) Bootstrapped(ctx context.Context) (bool, error)
- func (s *Service) ConfirmMFA(ctx context.Context, adminID, code string) ([]string, error)
- func (s *Service) CreateAdmin(ctx context.Context, email, password, role string) (PublicAdmin, error)
- func (s *Service) CreateTenant(ctx context.Context, req controlplane.RegisterRequest) (*controlplane.Tenant, error)
- func (s *Service) CreateUser(ctx context.Context, tenantID, email, password, role string) (PublicTenantUser, error)
- func (s *Service) DeleteTenant(ctx context.Context, id, confirm string) error
- func (s *Service) DeleteUser(ctx context.Context, tenantID, userID string) error
- func (s *Service) DisableMFA(ctx context.Context, adminID, code, password string) error
- func (s *Service) EnableMFA(ctx context.Context, adminID string) (secret, uri string, err error)
- func (s *Service) GetTenant(ctx context.Context, id string) (TenantDetail, error)
- func (s *Service) GetTenantSchema(ctx context.Context, id string) (*schema.APISchema, error)
- func (s *Service) IsTenantActive(ctx context.Context, id string) bool
- func (s *Service) ListData(ctx context.Context, tenantID, resource string, params url.Values) (map[string]any, error)
- func (s *Service) ListResources(ctx context.Context, tenantID string) ([]ResourceInfo, error)
- func (s *Service) ListRoles(ctx context.Context, tenantID string) ([]string, error)
- func (s *Service) ListTenants(ctx context.Context) ([]TenantInfo, error)
- func (s *Service) ListUsers(ctx context.Context, tenantID string) ([]PublicTenantUser, error)
- func (s *Service) Login(ctx context.Context, email, password string) (PlatformAuthResult, error)
- func (s *Service) MFAVerify(ctx context.Context, mfaToken, code string) (PlatformAuthResult, error)
- func (s *Service) PreviewTenantSchema(ctx context.Context, id string, sc *schema.APISchema, approved []string) (*migration.Preview, error)
- func (s *Service) Refresh(ctx context.Context, tokenStr string) (string, error)
- func (s *Service) Register(r chi.Router, obs ObsHandler, adminKey string)
- func (s *Service) SetFileStore(store *files.Store, maxBytes int64, ttl time.Duration, secret []byte)
- func (s *Service) SetTenantDB(tdb *db.TenantDB)
- func (s *Service) SetUserSuspended(ctx context.Context, tenantID, userID string, suspended bool) error
- func (s *Service) SuspendTenant(ctx context.Context, id string) error
- func (s *Service) UpdateUserRole(ctx context.Context, tenantID, userID, role string) error
- type Store
- func (s *Store) ConfirmMFA(ctx context.Context, adminID string, codeHashes []string) error
- func (s *Store) ConsumeBackupCode(ctx context.Context, adminID, codeHash string) (bool, error)
- func (s *Store) CountAdmins(ctx context.Context) (int, error)
- func (s *Store) CreateAdmin(ctx context.Context, email, passwordHash, role string) (Admin, error)
- func (s *Store) DisableMFA(ctx context.Context, adminID string) error
- func (s *Store) GetAdminByEmail(ctx context.Context, email string) (Admin, error)
- func (s *Store) GetAdminByID(ctx context.Context, id string) (Admin, error)
- func (s *Store) GetMFA(ctx context.Context, adminID string) (encSecret string, enabled, found bool, err error)
- func (s *Store) MFAEnabled(ctx context.Context, adminID string) (bool, error)
- func (s *Store) UpsertMFASecret(ctx context.Context, adminID, encSecret string) error
- type TenantDetail
- type TenantInfo
Constants ¶
const DefaultSuperAdminRole = "platform_super_admin"
DefaultSuperAdminRole is the role stamped on a platform admin. It is a marker inside the platform JWT (scope=platform), distinct from any tenant RBAC role.
const SystemSchema = "appximo_system"
SystemSchema is the Postgres schema that holds platform-level identity. It sits ABOVE every tenant_<id> schema: a platform admin is not a tenant user. The name is a fixed identifier (never tenant-derived), so it can never collide with a tenant schema (those are always tenant_<id>).
Variables ¶
var ( ErrInvalidCredentials = errors.New("platformadmin: invalid credentials") ErrTooManyAttempts = errors.New("platformadmin: too many attempts") ErrWeakPassword = errors.New("platformadmin: password too short") ErrInvalidEmail = errors.New("platformadmin: invalid email") ErrMFANotEnrolled = errors.New("platformadmin: mfa not enrolled") ErrMFAInvalidCode = errors.New("platformadmin: invalid mfa code") ErrMFAConfig = errors.New("platformadmin: mfa not configured") ErrUnknownRole = errors.New("platformadmin: role not declared in the schema RBAC") ErrTenantNotFound = errors.New("platformadmin: tenant not found") ErrConfirmRequired = errors.New("platformadmin: explicit confirmation required") // ErrAlreadyBootstrapped guards the first-run bootstrap: once ANY platform // admin exists, the bootstrap route is permanently closed (admins are then // created by an authenticated admin, or the CLI). ErrAlreadyBootstrapped = errors.New("platformadmin: an admin already exists") // ErrSchemaRejected marks a PersistBootSchema failure caused by an INVALID // schema — nothing was persisted and no restart happens (→ 422). Any other // persist failure (e.g. an unwritable schema file) is an infrastructure 500. ErrSchemaRejected = errors.New("schema rejected") )
Sentinel errors mapped to HTTP status by the handlers.
var ( ErrAdminEmailTaken = errors.New("platformadmin: admin email already registered") ErrAdminNotFound = errors.New("platformadmin: admin not found") )
ErrAdminEmailTaken is returned by CreateAdmin when the email already exists in the system schema. ErrAdminNotFound when no admin matches a lookup.
var ErrPlatformToken = errors.New("platformadmin: invalid or missing platform token")
ErrPlatformToken is returned when a platform token is missing/invalid/expired or is not actually a platform-scoped token (e.g. a tenant token presented to an admin route).
var ErrResourceNotFound = errors.New("platformadmin: resource not found")
ErrResourceNotFound is returned when a tenant has no such resource (or no schema).
Functions ¶
This section is empty.
Types ¶
type Admin ¶
type Admin struct {
ID string
Email string
PasswordHash string
Role string
EmailVerified bool
CreatedAt time.Time
UpdatedAt time.Time
}
Admin is a stored platform super-admin. PasswordHash is never serialized to a client and never logged (handlers build their own response shape).
type Config ¶
type Config struct {
// JWTSecret signs platform tokens — the SAME engine secret, one signing key.
JWTSecret string
// MFAKey encrypts the TOTP secret at rest (falls back to JWTSecret).
MFAKey string
// MFAIssuer is the authenticator-app label (default "Appximo Platform").
MFAIssuer string
// SuperAdminRole is the role stamped on platform admins (default
// platform_super_admin). It is a platform marker, not a tenant RBAC role.
SuperAdminRole string
// MinPasswordLength for a new admin (default 12 — a privileged credential).
MinPasswordLength int
// RoleExists reports whether a role name is declared in the (boot) schema RBAC.
// Used to validate a role before assigning it to a tenant user (so the admin API
// returns a clear error instead of creating a user who can never be authorized).
// nil ⇒ no validation (any role accepted).
RoleExists func(role string) bool
// TenantAdminRole reports whether a tenant RBAC role is "admin-grade" — broad
// enough that a holder may view their OWN tenant's observability. nil ⇒ no tenant
// user is treated as an observability admin (only the platform super-admin sees
// observability). Implemented over the boot RBAC policy (inherits RBAC).
TenantAdminRole func(role string) bool
// ServedResources is the set of resource names the engine serves LIVE — the REST
// routes, GraphQL types and RBAC policy are all compiled from the BOOT --schema
// (codegen.BuildRouter / gqlhandler.BuildHandler / rbac.RBACMiddleware), globally
// and at boot. A resource deployed to a tenant but ABSENT here gets its tables
// provisioned by the migration, but its API is unavailable (403 from RBAC deny-by-
// default, or 404 when a wildcard role passes RBAC but no route exists) until the
// engine restarts with a schema that includes it. Exposed
// read-only at GET /admin/served-resources so the editor can honestly warn before
// a new-resource deploy. Empty ⇒ the route reports an empty set.
ServedResources []string
// FlowHandlerFn, when non-nil, supplies the app's LIVE data-plane router for
// the flow-test runner (FLOWTEST-S1) — flows execute against the real chain
// (tenant → JWT → RBAC → generated routes), in-process. It is a function
// because the router is (re)built at boot and on every hot-swap; nil while
// the engine is still booting ⇒ the run endpoints answer 503.
FlowHandlerFn func() http.Handler
// ServedResourcesFn, when non-nil, supplies the LIVE served-resource list and
// takes precedence over the static ServedResources slice. It exists for the
// in-process fleet hot-swap (MT-STRUCT-S4): after a per-app hot-swap the served
// surface changes WITHOUT a process restart, so the editor's post-deploy verify
// must read the current surface's resources, not the boot list. Single-engine
// leaves it nil (a deploy re-execs, so the boot slice is authoritative).
ServedResourcesFn func() []string
// ActivationFn, when non-nil, reports HOW a schema deploy activates on this
// engine: "hot_swap" (in-process fleet, MT-STRUCT-S4/S5 — only this app is
// recompiled and swapped, no downtime) or "restart" (single-engine graceful
// re-exec, ~6 s). Surfaced in GET /admin/served-resources so the editor's
// deploy UI describes the real activation instead of always saying "restart".
// nil ⇒ "restart" (the historical behavior).
ActivationFn func() string
// PersistBootSchema validates a schema and ATOMICALLY persists it as the new
// BOOT schema (the file the engine loads at start), backing up the previous
// one for rollback (UI-F4-S2). An invalid schema must be reported wrapped in
// ErrSchemaRejected (→ 422) with NOTHING written. nil ⇒ the engine cannot
// self-restart (POST /admin/engine/schema answers 503).
PersistBootSchema func(raw json.RawMessage) error
// TriggerRestart initiates the engine's graceful self-restart (drain via
// readyz→503 + http.Server.Shutdown, then relaunch with the persisted
// schema). Called only AFTER PersistBootSchema succeeded — never with an
// invalid schema.
TriggerRestart func()
}
Config configures the platform admin Service.
type ObsHandler ¶
type ObsHandler interface {
ServeTenantData(w http.ResponseWriter, r *http.Request)
}
ObsHandler is the slice of the observability server the admin API needs: serve one tenant's observability JSON (already tenant-scoped). Declared as an interface so this package does not import pkg/observability (and to keep it mockable).
type PlatformAuthResult ¶
type PlatformAuthResult struct {
Admin PublicAdmin `json:"admin,omitempty"`
Token string `json:"token,omitempty"`
MFARequired bool `json:"mfa_required,omitempty"`
MFAToken string `json:"mfa_token,omitempty"`
}
PlatformAuthResult is returned by Login. On a password-only success Token is set; when the admin has MFA, Token is withheld and MFARequired+MFAToken are set.
type PlatformClaims ¶
type PlatformClaims struct {
AdminID string `json:"aid"`
Role string `json:"prole"`
Scope string `json:"scope"`
jwt.RegisteredClaims
}
PlatformClaims is the payload of a platform super-admin JWT. Its claim KEYS (aid/prole/scope) deliberately DIFFER from auth.Claims (user_id/role/tenant_id):
- A platform token presented to a tenant /api/ route parses (same HS256 secret) but yields an empty auth.Claims (no user_id/role/tenant_id) → the engine RBAC denies it (deny by default). So a platform JWT can NEVER act as a tenant without the super-admin EXPLICITLY selecting a tenant through the admin API.
- A tenant token presented to an admin route has no "scope" claim → it fails requirePlatformAdmin (403). The two token worlds cannot be confused.
It is signed with the engine's JWT_SECRET (one signing key, one validator family — HS256, exp required), exactly like every other token the engine issues.
type PublicAdmin ¶
type PublicAdmin struct {
ID string `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
}
PublicAdmin is the admin shape returned to clients — never the password hash.
type PublicTenantUser ¶
type PublicTenantUser struct {
ID string `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
EmailVerified bool `json:"email_verified"`
Suspended bool `json:"suspended"`
CreatedAt time.Time `json:"created_at"`
}
PublicTenantUser is a tenant user as seen by the admin API (no hash).
type ResourceField ¶
ResourceField is one column descriptor (name + declared type) for the data UI.
type ResourceHandler ¶ added in v0.1.14
type ResourceHandler interface {
ServeResources(w http.ResponseWriter, r *http.Request)
ServeResourcesSnapshot(w http.ResponseWriter, r *http.Request)
}
ResourceHandler is the OPTIONAL second slice of the observability server: the engine's own resources + attribution verdict (CENTINELA-C-S1). Optional (a type assertion, not a widening of ObsHandler) so a test stub that only serves tenant data keeps compiling; an ObsHandler that implements it gets the /admin/resources routes registered.
type ResourceInfo ¶
type ResourceInfo struct {
Name string `json:"name"`
Fields []ResourceField `json:"fields"`
}
ResourceInfo is a tenant resource the operator can browse.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the platform admin backend: super-admin auth (login + MFA) and the consolidated tenant / user / observability admin operations.
func NewService ¶
func NewService(store *Store, users *userauth.Store, cp controlplane.Service, pool *pgxpool.Pool, cfg Config) *Service
NewService builds a platform admin Service. cp wraps the existing control plane (tenant create/lookup); users is the existing per-tenant identity store.
func (*Service) ActivateTenant ¶
ActivateTenant clears the suspension flag.
func (*Service) ApplyTenantSchema ¶
func (s *Service) ApplyTenantSchema(ctx context.Context, id string, sc *schema.APISchema, approved []string) (*migration.ApplyOutcome, error)
ApplyTenantSchema applies sc to tenant id, executing ONLY the destructive drops whose key is enumerated in approved (everything else stays gated as drift — the fail-safe default). Returns the outcome (applied/gated/unmatched). This is the SAME approved-apply path the control plane's PUT /tenants/{id}/schema uses.
func (*Service) Bootstrap ¶ added in v0.1.2
func (s *Service) Bootstrap(ctx context.Context, email, password string) (PlatformAuthResult, error)
Bootstrap creates the FIRST platform admin and signs them in (PHASE4-FIRST-MILE-S1). It exists because the first-run /admin experience was a dead end: a login form with no way to obtain credentials except knowing the CLI. The route that calls this is gated by the ADMIN_KEY (the operator's own boot credential — the same trust level as shell access, where `appximo admin create` already lives) and closes permanently once any admin exists.
func (*Service) Bootstrapped ¶ added in v0.1.2
Bootstrapped reports whether at least one platform admin exists. It backs the unauthenticated /admin/auth/status probe the login screen uses to decide between "sign in" and "create the first admin" — after bootstrap it is a constant true, so it discloses nothing an attacker can use.
func (*Service) ConfirmMFA ¶
ConfirmMFA validates the first code and activates MFA, returning backup codes.
func (*Service) CreateAdmin ¶
func (s *Service) CreateAdmin(ctx context.Context, email, password, role string) (PublicAdmin, error)
CreateAdmin provisions a platform super-admin. It is the BOOTSTRAP path (no public signup): the first admin is created via the CLI (gated by the admin key / direct DB access), because a super-admin cannot authenticate with a super-admin that does not yet exist. role defaults to the configured super-admin role.
func (*Service) CreateTenant ¶
func (s *Service) CreateTenant(ctx context.Context, req controlplane.RegisterRequest) (*controlplane.Tenant, error)
CreateTenant wraps the EXISTING control plane (controlplane.Register) — it does not reimplement tenant provisioning. The schema is validated identically to the :9090 path before provisioning.
func (*Service) CreateUser ¶
func (s *Service) CreateUser(ctx context.Context, tenantID, email, password, role string) (PublicTenantUser, error)
CreateUser creates a tenant user with an admin-chosen role (validated against the schema RBAC). Unlike public signup, the role is explicit.
func (*Service) DeleteTenant ¶
DeleteTenant DESTRUCTIVELY removes a tenant: it requires an explicit confirmation equal to the tenant id (so a stray DELETE can never wipe a tenant), then drops the tenant's Postgres schema (CASCADE — all its data) and deletes its control- plane rows. There is no undo.
func (*Service) DeleteUser ¶
DeleteUser removes a tenant user.
func (*Service) DisableMFA ¶
DisableMFA turns MFA off, but only with a valid second factor (TOTP/backup) or the admin's password — never the session token alone.
func (*Service) EnableMFA ¶
EnableMFA begins enrollment for an authenticated admin: generates a TOTP secret, stores it encrypted (enabled=false), returns the secret + otpauth URI once.
func (*Service) GetTenantSchema ¶
GetTenantSchema returns a tenant's stored schema (for loading it back into the visual editor). nil when the tenant exists but has no schema set yet.
func (*Service) IsTenantActive ¶
IsTenantActive reports whether a tenant exists and is not suspended. It is the predicate wired into userauth login (TenantActive): a suspended tenant cannot mint new sessions. One PK lookup, on the login path only (never the CRUD hot path). A tenant with no public.tenants row is treated as active (login then fails on the missing user anyway) to avoid coupling the engine's naming-convention tenant model to a hard registry check.
func (*Service) ListData ¶
func (s *Service) ListData(ctx context.Context, tenantID, resource string, params url.Values) (map[string]any, error)
ListData returns a page of a tenant resource's records (READ-ONLY browse). It REUSES the engine's validated, injection-safe query builder (filters/sort/keyset pagination from `params`) and runs it through the tenant-scoped DB — no new query logic, and the tenant_<id> search_path is the isolation boundary. Authorization is the platform super-admin (any tenant); a record's columns are whatever the resource declares. `meta.has_next` uses the keyset cursor convention (fetch per_page; the UI loads more via ?after=<last id>).
func (*Service) ListResources ¶
ListResources returns the tenant's resources (from its STORED schema — the engine already knows them; this does not re-derive anything) with each field's declared type, so the data UI can render typed columns. The implicit `id` UUID PK is listed first.
func (*Service) ListRoles ¶
ListRoles returns the RBAC role names declared in the tenant's stored schema, so the user-management UI can offer a role picker (it only ASSIGNS existing roles — creating roles is the schema editor, out of scope). Sorted.
func (*Service) ListTenants ¶
func (s *Service) ListTenants(ctx context.Context) ([]TenantInfo, error)
ListTenants returns every registered tenant (newest first) with cheap metadata. Row/user counts come from pg_stat_user_tables estimates (auth_ tables split out of data_rows), exactly like the `appximo tenant list` CLI inventory.
func (*Service) Login ¶
Login verifies a platform admin's credentials and returns a platform token (or an MFA challenge). Uniform on unknown-email vs wrong-password (anti-enumeration, equalized timing). Throttled per email.
func (*Service) MFAVerify ¶
MFAVerify completes a login's second factor and mints the final platform token.
func (*Service) PreviewTenantSchema ¶
func (s *Service) PreviewTenantSchema(ctx context.Context, id string, sc *schema.APISchema, approved []string) (*migration.Preview, error)
PreviewTenantSchema computes the DRY-RUN migration plan for applying sc to tenant id — the safe ops, the data-losing drops with their measured impact (rows lost), the drift and the concerns — evaluated against the given destructive-approval set. It applies NOTHING (the informed-consent surface the destructive gate needs).
func (*Service) Register ¶
func (s *Service) Register(r chi.Router, obs ObsHandler, adminKey string)
Register wires the admin API onto the data-plane router r. Routes live under /admin/ (already JWT-skipped and RBAC-passthrough — they do their OWN auth) and are registered individually so they coexist with the existing /admin/backup and /admin/tenants/{id}/reload machine endpoints (no Mount collision). adminKey lets machine callers (DevHub, scripts) use X-Admin-Key on the management routes — the "two paths for two consumers" rule: humans log in, machines present the key. The /admin/auth/* identity routes never accept the key (a human session needs an identity). obs may be nil (observability route then returns 503).
func (*Service) SetFileStore ¶
func (s *Service) SetFileStore(store *files.Store, maxBytes int64, ttl time.Duration, secret []byte)
SetFileStore wires the engine's file store so the admin API can manage a tenant's files (the Studio files manager). Set by app.go after NewService — like SetTenantDB. When unset, the files routes answer 503 (the CLI/bootstrap path never needs them). secret signs the short-lived download tokens (the SAME engine JWT secret files.SignedURLHandler uses); ttl bounds both those tokens and S3 presigned URLs.
func (*Service) SetTenantDB ¶
SetTenantDB wires the engine's tenant-scoped DB so the admin API can browse a tenant's DATA (read-only). It is set by app.go after NewService. When unset, the data-browsing endpoints return an error (the CLI/bootstrap path never needs it).
func (*Service) SetUserSuspended ¶
func (s *Service) SetUserSuspended(ctx context.Context, tenantID, userID string, suspended bool) error
SetUserSuspended toggles a tenant user's administrative lockout.
func (*Service) SuspendTenant ¶
SuspendTenant / ActivateTenant flip the control-plane suspended flag. Suspension blocks NEW logins for the tenant's users (enforced on the non-hot login path via the TenantActive predicate wired in app.go); already-issued JWTs remain valid until exp (the documented stateless-JWT trade-off). It deliberately adds NO per-request check to the CRUD/JWT hot path, preserving the measured p50.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store persists platform admins (and their MFA enrollment) in the system schema, using the engine's shared pool. DDL is run once per process (lazy, behind a sync.Once-ish guard). It mirrors pkg/userauth's Store shape so the two identity surfaces look and behave the same.
func (*Store) ConfirmMFA ¶
ConfirmMFA flips enabled=true and stores the backup-code hashes atomically.
func (*Store) ConsumeBackupCode ¶
ConsumeBackupCode marks a matching unused backup code as used and returns true. false when no unused code matches the hash. Consumption is atomic (a code is one-time).
func (*Store) CountAdmins ¶
CountAdmins returns the number of platform admins (used by bootstrap to refuse a public/duplicate first-admin path).
func (*Store) CreateAdmin ¶
CreateAdmin inserts a platform admin. email is stored as given (the caller normalizes); a case-insensitive duplicate → ErrAdminEmailTaken. The returned Admin has no PasswordHash set.
func (*Store) DisableMFA ¶
DisableMFA clears an admin's secret and backup codes.
func (*Store) GetAdminByEmail ¶
GetAdminByEmail looks an admin up by email (case-insensitive). ErrAdminNotFound when none matches.
func (*Store) GetAdminByID ¶
GetAdminByID looks an admin up by id. ErrAdminNotFound when none matches.
func (*Store) GetMFA ¶
func (s *Store) GetMFA(ctx context.Context, adminID string) (encSecret string, enabled, found bool, err error)
GetMFA returns the admin's encrypted secret and enabled flag. found=false when there is no enrollment row.
func (*Store) MFAEnabled ¶
MFAEnabled reports whether the admin has confirmed (active) MFA.
type TenantDetail ¶
type TenantDetail struct {
TenantInfo
}
TenantDetail is a single tenant; its UserCount is exact (one per-tenant query), unlike the list's estimate.
type TenantInfo ¶
type TenantInfo struct {
ID string `json:"id"`
DisplayName string `json:"display_name"`
Email string `json:"email"`
Plan string `json:"plan"`
Suspended bool `json:"suspended"`
CreatedAt time.Time `json:"created_at"`
ResourceCount int `json:"resource_count"`
DataRows int64 `json:"data_rows"`
UserCount int64 `json:"user_count"`
}
TenantInfo is the per-tenant summary returned by the tenant list. resource_count is derived from the stored schema (free); data_rows and user_count are pg_stat n_live_tup ESTIMATES (the same inventory `appximo tenant list` prints) — free at list time, never a per-tenant exact COUNT.