feat(ui): add identity management for tenants and users
CI / build-and-test (push) Successful in 1m47s
CI / build-and-test (push) Successful in 1m47s
* Implement tenant and user creation in IdentityPage * Add API calls for managing tenants and users * Introduce tenant-scoped API requests * Update sidebar to include identity navigation * Create BooleanStatusBadge component for key status
This commit is contained in:
@@ -7,9 +7,9 @@ Gitea issue #10 asks AMCS to isolate memories by user, tenant, or workspace. The
|
||||
Observed code paths:
|
||||
|
||||
- Authentication enters through `internal/auth/middleware.go`. API-key auth uses the configured header, defaulting to `x-brain-key`; bearer tokens can resolve through OAuth `TokenStore` or API keyring; HTTP Basic resolves OAuth client credentials.
|
||||
- `internal/auth/middleware.go` writes both `auth.key_id` and `tenancy.tenant_key` into the request context. The tenant key is intentionally opaque and currently equals the authenticated key id or OAuth client id.
|
||||
- `internal/auth/middleware.go` writes both `auth.key_id` and `tenancy.tenant_id` into the request context. The tenant key is intentionally opaque and currently equals the authenticated key id or OAuth client id.
|
||||
- `internal/tenancy/tenancy.go` exposes `WithTenantKey` and `KeyFromContext` only; callers should not infer user semantics from the tenant string.
|
||||
- Schema already has `tenant_key` on `projects`, `thoughts`, `stored_files`, `learnings`, `plans`, and `chat_histories` in `schema/*.dbml`.
|
||||
- Schema already has `tenant_id` on `projects`, `thoughts`, `stored_files`, `learnings`, `plans`, and `chat_histories` in `schema/*.dbml`.
|
||||
- `internal/store/tenancy.go` provides helper functions for appending tenant predicates to SQL.
|
||||
- Tenant scoping is already present in project, thought, and stored-file store paths. Some other project-owned domains still need explicit tenant enforcement.
|
||||
|
||||
@@ -17,9 +17,9 @@ Observed code paths:
|
||||
|
||||
Use the authenticated principal id as the tenant boundary:
|
||||
|
||||
1. API key: resolve token through `auth.Keyring.Lookup`; use returned key id as `tenant_key`.
|
||||
2. OAuth bearer token: resolve token through `TokenStore.Lookup`; use returned client id/key id as `tenant_key`.
|
||||
3. OAuth Basic client credentials: resolve through `OAuthRegistry.Lookup`; use returned client id as `tenant_key`.
|
||||
1. API key: resolve token through `auth.Keyring.Lookup`; use returned key id as `tenant_id`.
|
||||
2. OAuth bearer token: resolve token through `TokenStore.Lookup`; use returned client id/key id as `tenant_id`.
|
||||
3. OAuth Basic client credentials: resolve through `OAuthRegistry.Lookup`; use returned client id as `tenant_id`.
|
||||
4. Unauthenticated requests must not get tenant context and must not reach protected MCP/API handlers.
|
||||
|
||||
Do not store raw API keys or bearer tokens in tenant columns. Store only stable configured key ids/client ids. Yes, it is less flashy than inventing an account service before breakfast, but it keeps the trust boundary small and auditable.
|
||||
@@ -28,7 +28,7 @@ Do not store raw API keys or bearer tokens in tenant columns. Store only stable
|
||||
|
||||
Source-of-truth DBML changes:
|
||||
|
||||
- Add nullable `tenant_key text` to all user-owned tables:
|
||||
- Add nullable `tenant_id text` to all user-owned tables:
|
||||
- `projects`
|
||||
- `thoughts`
|
||||
- `stored_files`
|
||||
@@ -36,9 +36,9 @@ Source-of-truth DBML changes:
|
||||
- `plans`
|
||||
- `chat_histories`
|
||||
- Add tenant indexes:
|
||||
- single-column `tenant_key` for direct filtering
|
||||
- `(tenant_key, name)` unique on `projects`, replacing global `projects.name` uniqueness
|
||||
- `(tenant_key, project_id)` on `thoughts` for common project-scoped memory lookups
|
||||
- single-column `tenant_id` for direct filtering
|
||||
- `(tenant_id, name)` unique on `projects`, replacing global `projects.name` uniqueness
|
||||
- `(tenant_id, project_id)` on `thoughts` for common project-scoped memory lookups
|
||||
- Regenerate SQL migrations and generated models from DBML; do not hand-edit generated Go models except as a temporary debugging step.
|
||||
|
||||
Important follow-up: `agent_skills`, `agent_guardrails`, `agent_personas`, `agent_parts`, traits, and arcs are currently global catalogs. Keep them global unless product requirements say skills/personas are private per tenant. Project join tables inherit protection through tenant-scoped project ids, but direct join queries must verify the project belongs to the tenant.
|
||||
@@ -49,9 +49,9 @@ All request-facing store methods that touch tenant-owned rows must include tenan
|
||||
|
||||
Rules:
|
||||
|
||||
- Inserts must populate `tenant_key` from context.
|
||||
- Gets/updates/deletes by id or guid must add `and tenant_key = $n`.
|
||||
- Lists/searches must add `tenant_key = $n` before other filters.
|
||||
- Inserts must populate `tenant_id` from context.
|
||||
- Gets/updates/deletes by id or guid must add `and tenant_id = $n`.
|
||||
- Lists/searches must add `tenant_id = $n` before other filters.
|
||||
- Joins must scope the tenant-owned root table. Example: project summaries should count only thoughts that belong to the same tenant as the project, not merely thoughts with the same `project_id`.
|
||||
- Background maintenance jobs must either run per tenant, carry tenant context explicitly, or intentionally operate cross-tenant with an internal-only code path documented in the job.
|
||||
|
||||
@@ -74,24 +74,24 @@ Paths needing audit/hardening:
|
||||
|
||||
Migration approach for existing single-tenant installs:
|
||||
|
||||
1. Add nullable `tenant_key` columns first; do not make them `not null` initially because existing deployments have historical rows without a principal.
|
||||
2. Backfill existing rows to a configured default tenant only if the instance enables multi-tenant mode or defines `auth.default_tenant_key`. Otherwise leave `NULL` rows visible only to unauthenticated/internal single-tenant contexts.
|
||||
3. Replace global project-name uniqueness with `(tenant_key, name)` uniqueness. For PostgreSQL, preserve legacy `NULL` semantics carefully: multiple null-tenant projects with the same name may be possible unless a partial unique index is added for null tenant rows.
|
||||
1. Add nullable `tenant_id` columns first; do not make them `not null` initially because existing deployments have historical rows without a principal.
|
||||
2. Backfill existing rows to a configured default tenant only if the instance enables multi-tenant mode or defines `auth.default_tenant_id`. Otherwise leave `NULL` rows visible only to unauthenticated/internal single-tenant contexts.
|
||||
3. Replace global project-name uniqueness with `(tenant_id, name)` uniqueness. For PostgreSQL, preserve legacy `NULL` semantics carefully: multiple null-tenant projects with the same name may be possible unless a partial unique index is added for null tenant rows.
|
||||
4. Add indexes concurrently where practical for production-sized tables.
|
||||
5. Document that after enabling auth-backed tenancy, legacy null-tenant data is not visible to authenticated tenants unless backfilled.
|
||||
|
||||
Recommended config addition:
|
||||
|
||||
- `auth.default_tenant_key` or `tenancy.default_key` for one-time/self-hosted backfill and development.
|
||||
- `auth.default_tenant_id` or `tenancy.default_key` for one-time/self-hosted backfill and development.
|
||||
- Optional `tenancy.mode: single|authenticated` so operators can keep current single-user behavior deliberately instead of discovering isolation by accident. An accident in auth is just a breach with better branding.
|
||||
|
||||
## Authorization checks
|
||||
|
||||
Authorization is row ownership by tenant key:
|
||||
|
||||
- A request may only see or mutate rows whose `tenant_key` equals the authenticated tenant key.
|
||||
- A request may only see or mutate rows whose `tenant_id` equals the authenticated tenant key.
|
||||
- Cross-tenant ids/gids should behave as not found, not forbidden, to avoid existence leaks.
|
||||
- Tenant key is server-derived only. Ignore any client-provided `tenant_key` fields on tool/API inputs.
|
||||
- Tenant key is server-derived only. Ignore any client-provided `tenant_id` fields on tool/API inputs.
|
||||
- Project id references in create/update operations must be validated against the same tenant before use. This prevents attaching a new thought/file/plan to another tenant's project id.
|
||||
- Relationship operations must verify both endpoints are in the same tenant before creating links.
|
||||
- Global catalogs can be read across tenants only if intentionally shared; project-specific associations must be tenant-checked through the project.
|
||||
@@ -117,7 +117,7 @@ HTTP/API boundaries:
|
||||
UI:
|
||||
|
||||
- Project selector/list should naturally show tenant-scoped projects.
|
||||
- Admin tables for projects/thoughts/files/learnings/plans/chat histories must not show `tenant_key` as an editable field.
|
||||
- Admin tables for projects/thoughts/files/learnings/plans/chat histories must not show `tenant_id` as an editable field.
|
||||
- If a tenant switcher is ever added, it must map to a server-side authenticated principal or admin impersonation path, not a client-side query parameter. Obviously.
|
||||
|
||||
## Test cases
|
||||
@@ -143,7 +143,7 @@ Minimum test matrix:
|
||||
Assumptions:
|
||||
|
||||
- The first tenancy boundary is authenticated principal id, not human account, org, or workspace. This is consistent with the current auth system and avoids adding an account model prematurely.
|
||||
- Null `tenant_key` remains the compatibility path for existing single-tenant/internal flows.
|
||||
- Null `tenant_id` remains the compatibility path for existing single-tenant/internal flows.
|
||||
- Global skills/personas/guardrails remain shared catalogs until a separate product decision makes them tenant-private.
|
||||
|
||||
Blockers/decisions needed:
|
||||
|
||||
Reference in New Issue
Block a user