diff --git a/docs/tech-specs/capabilities.md b/docs/tech-specs/capabilities.md index 7717cbc9..ba27c738 100644 --- a/docs/tech-specs/capabilities.md +++ b/docs/tech-specs/capabilities.md @@ -100,6 +100,7 @@ multi-word subsystems. | `users:admin` | Assign / remove roles on users within the workspace | | `keys:self` | Create / revoke / list **own** API keys | | `keys:admin` | Create / revoke / list **any user's** API keys within the workspace | +| `workspaces:list-own` | List workspaces the caller has access to | | `workspaces:admin` | Create / delete / disable workspaces (system-level) | | `iam:admin` | JWT signing-key rotation, IAM-level operations | | `metrics:read` | Prometheus metrics proxy | @@ -110,7 +111,7 @@ The open-source edition ships three roles: | Role | Capabilities | |---|---| -| `reader` | `agent`, `graph:read`, `documents:read`, `rows:read`, `llm`, `embeddings`, `mcp`, `collections:read`, `knowledge:read`, `flows:read`, `config:read`, `keys:self` | +| `reader` | `agent`, `graph:read`, `documents:read`, `rows:read`, `llm`, `embeddings`, `mcp`, `collections:read`, `knowledge:read`, `flows:read`, `config:read`, `keys:self`, `workspaces:list-own` | | `writer` | everything in `reader` **+** `graph:write`, `documents:write`, `rows:write`, `collections:write`, `knowledge:write` | | `admin` | everything in `writer` **+** `config:write`, `flows:write`, `users:read`, `users:write`, `users:admin`, `keys:admin`, `workspaces:admin`, `iam:admin`, `metrics:read` | diff --git a/docs/tech-specs/iam-protocol.md b/docs/tech-specs/iam-protocol.md index e7e7984e..46b3f569 100644 --- a/docs/tech-specs/iam-protocol.md +++ b/docs/tech-specs/iam-protocol.md @@ -224,6 +224,7 @@ class ApiKeyRecord: | `enable-user` | `user_id`, `workspace` (optional integrity check) | — | Re-enables a previously disabled user; does not restore API keys. | | `delete-user` | `user_id`, `workspace` (optional integrity check) | — | Hard-delete; removes user record, username lookup, and all the user's API keys. | | `create-workspace` | `workspace_record` | `workspace` | System-level. | +| `list-my-workspaces` | `actor` (gateway-injected) | `workspaces` | Returns the workspaces the calling user has access to. OSS: the user's home workspace; if the caller holds the `admin` role, returns all workspaces instead. Enterprise regimes return whatever workspaces the user has been granted access to. | | `list-workspaces` | — | `workspaces` | System-level. | | `get-workspace` | `workspace_record` (id only) | `workspace` | System-level. | | `update-workspace` | `workspace_record` | `workspace` | System-level. | diff --git a/specs/api/components/schemas/iam/ApiKeyInput.yaml b/specs/api/components/schemas/iam/ApiKeyInput.yaml new file mode 100644 index 00000000..257cfb31 --- /dev/null +++ b/specs/api/components/schemas/iam/ApiKeyInput.yaml @@ -0,0 +1,21 @@ +type: object +description: | + API key creation fields. Used with `create-api-key`. +properties: + user_id: + type: string + description: User to create the key for. + examples: + - usr_abc123 + name: + type: string + description: Operator-facing label for the key (e.g. "laptop", "CI"). + examples: + - laptop + expires: + type: string + description: | + Optional expiry timestamp in ISO-8601 UTC. Empty string or + omitted means the key does not expire. + examples: + - "2027-01-01T00:00:00Z" diff --git a/specs/api/components/schemas/iam/ApiKeyRecord.yaml b/specs/api/components/schemas/iam/ApiKeyRecord.yaml new file mode 100644 index 00000000..728c1f50 --- /dev/null +++ b/specs/api/components/schemas/iam/ApiKeyRecord.yaml @@ -0,0 +1,38 @@ +type: object +description: API key record returned by IAM operations. +properties: + id: + type: string + description: Key identifier. + examples: + - key_xyz789 + user_id: + type: string + description: Owning user identifier. + examples: + - usr_abc123 + name: + type: string + description: Operator-facing label. + examples: + - laptop + prefix: + type: string + description: | + First 4 characters of the plaintext key, for identification + in listings. Never enough to reconstruct the key. + examples: + - tg_a + expires: + type: string + description: Expiry timestamp (ISO-8601 UTC). Empty if no expiry. + examples: + - "2027-01-01T00:00:00Z" + created: + type: string + description: Creation timestamp (ISO-8601 UTC). + examples: + - "2026-01-15T10:30:00Z" + last_used: + type: string + description: Last-used timestamp (ISO-8601 UTC). Empty if never used. diff --git a/specs/api/components/schemas/iam/IamRequest.yaml b/specs/api/components/schemas/iam/IamRequest.yaml new file mode 100644 index 00000000..b386c69a --- /dev/null +++ b/specs/api/components/schemas/iam/IamRequest.yaml @@ -0,0 +1,106 @@ +type: object +description: | + IAM service request. + + The IAM service is a **global service** — it operates at system level, + not scoped to a specific workspace. All operations are dispatched via + the `operation` field. + + Some operations require admin capabilities; others (like `whoami` and + `list-my-workspaces`) are available to any authenticated user. See + the capability vocabulary for details. + + The `actor` field is injected by the gateway and cannot be set by + the client. It identifies the authenticated caller. +required: + - operation +properties: + operation: + type: string + enum: + - whoami + - list-my-workspaces + - create-user + - list-users + - get-user + - update-user + - disable-user + - enable-user + - delete-user + - create-workspace + - list-workspaces + - get-workspace + - update-workspace + - disable-workspace + - create-api-key + - list-api-keys + - revoke-api-key + - reset-password + - rotate-signing-key + description: | + Operation to perform. + + **Any authenticated user:** + - `whoami`: Return the caller's own user record + - `list-my-workspaces`: List workspaces the caller has access to + + **User management (requires `users:read`/`users:write`/`users:admin`):** + - `create-user`: Create a new user in a workspace + - `list-users`: List users (optionally filtered by workspace) + - `get-user`: Get a specific user record + - `update-user`: Update user fields (name, email, roles, enabled) + - `disable-user`: Soft-disable a user and revoke their API keys + - `enable-user`: Re-enable a previously disabled user + - `delete-user`: Hard-delete a user and their API keys + + **Workspace management (requires `workspaces:admin`):** + - `create-workspace`: Create a new workspace + - `list-workspaces`: List all workspaces (admin view) + - `get-workspace`: Get a specific workspace record + - `update-workspace`: Update workspace name or enabled state + - `disable-workspace`: Disable workspace and all its users + + **API key management (requires `keys:self` or `keys:admin`):** + - `create-api-key`: Create an API key for a user + - `list-api-keys`: List API keys for a user + - `revoke-api-key`: Revoke (delete) an API key + + **Password management:** + - `reset-password`: Admin-initiated password reset (requires `users:admin`) + + **System (requires `iam:admin`):** + - `rotate-signing-key`: Rotate the JWT signing key + workspace: + type: string + description: | + Workspace scope. Required on workspace-scoped operations + (e.g. `create-user`). Acts as an optional integrity check on + operations that target a user or key — when supplied, the target's + home workspace must match. + + Omitted for system-level operations (`list-workspaces`, + `rotate-signing-key`) and for identity-resolution operations + (`whoami`, `list-my-workspaces`). + examples: + - default + - production + user_id: + type: string + description: | + Target user identifier. Required for operations that act on a + specific user: `get-user`, `update-user`, `disable-user`, + `enable-user`, `delete-user`, `reset-password`, `list-api-keys`. + examples: + - usr_abc123 + user: + $ref: './UserInput.yaml' + workspace_record: + $ref: './WorkspaceInput.yaml' + key: + $ref: './ApiKeyInput.yaml' + key_id: + type: string + description: | + API key identifier. Required for `revoke-api-key`. + examples: + - key_xyz789 diff --git a/specs/api/components/schemas/iam/IamResponse.yaml b/specs/api/components/schemas/iam/IamResponse.yaml new file mode 100644 index 00000000..58d6937e --- /dev/null +++ b/specs/api/components/schemas/iam/IamResponse.yaml @@ -0,0 +1,51 @@ +type: object +description: | + IAM service response. Fields are populated depending on the + operation that was invoked. +properties: + user: + $ref: './UserRecord.yaml' + users: + type: array + description: List of user records (populated by `list-users`). + items: + $ref: './UserRecord.yaml' + workspace: + $ref: './WorkspaceRecord.yaml' + workspaces: + type: array + description: | + List of workspace records (populated by `list-workspaces` and + `list-my-workspaces`). + items: + $ref: './WorkspaceRecord.yaml' + api_key_plaintext: + type: string + description: | + Plaintext API key. Returned **once** by `create-api-key`. + Never populated on any other operation. The caller must + capture this value — it cannot be retrieved again. + api_key: + $ref: './ApiKeyRecord.yaml' + api_keys: + type: array + description: List of API key records (populated by `list-api-keys`). + items: + $ref: './ApiKeyRecord.yaml' + temporary_password: + type: string + description: | + Temporary password returned once by `reset-password`. + error: + type: object + description: Error details (present on failure). + properties: + type: + type: string + description: | + Error type. One of: `invalid-argument`, `not-found`, + `duplicate`, `auth-failed`, `weak-password`, `disabled`, + `operation-not-permitted`, `internal-error`. + message: + type: string + description: Human-readable error description (not surfaced to end users). diff --git a/specs/api/components/schemas/iam/UserInput.yaml b/specs/api/components/schemas/iam/UserInput.yaml new file mode 100644 index 00000000..9efec490 --- /dev/null +++ b/specs/api/components/schemas/iam/UserInput.yaml @@ -0,0 +1,42 @@ +type: object +description: | + User creation/update fields. Used with `create-user` and `update-user`. + The `password` field is only accepted on `create-user`. +properties: + username: + type: string + description: Login username. Unique within a workspace. + examples: + - alice + name: + type: string + description: Display name. + examples: + - Alice Smith + email: + type: string + description: Email address. + examples: + - alice@example.com + password: + type: string + description: | + Initial password. Only accepted on `create-user`; rejected on + `update-user`. Use `reset-password` or `change-password` to + modify passwords. + roles: + type: array + items: + type: string + description: | + Roles to assign. Open-source roles: `reader`, `writer`, `admin`. + examples: + - - reader + enabled: + type: boolean + description: Whether the user is enabled. + default: true + must_change_password: + type: boolean + description: Force password change on next login. + default: false diff --git a/specs/api/components/schemas/iam/UserRecord.yaml b/specs/api/components/schemas/iam/UserRecord.yaml new file mode 100644 index 00000000..0e59f7e0 --- /dev/null +++ b/specs/api/components/schemas/iam/UserRecord.yaml @@ -0,0 +1,46 @@ +type: object +description: User record returned by IAM operations. +properties: + id: + type: string + description: Unique user identifier. + examples: + - usr_abc123 + workspace: + type: string + description: User's home workspace. + examples: + - default + username: + type: string + description: Login username (unique within workspace). + examples: + - alice + name: + type: string + description: Display name. + examples: + - Alice Smith + email: + type: string + description: Email address. + examples: + - alice@example.com + roles: + type: array + items: + type: string + description: Assigned roles. + examples: + - - reader + enabled: + type: boolean + description: Whether the user is enabled. + must_change_password: + type: boolean + description: Whether the user must change password on next login. + created: + type: string + description: Creation timestamp (ISO-8601 UTC). + examples: + - "2026-01-15T10:30:00Z" diff --git a/specs/api/components/schemas/iam/WorkspaceInput.yaml b/specs/api/components/schemas/iam/WorkspaceInput.yaml new file mode 100644 index 00000000..d6e5b04d --- /dev/null +++ b/specs/api/components/schemas/iam/WorkspaceInput.yaml @@ -0,0 +1,23 @@ +type: object +description: | + Workspace creation/update fields. Used with `create-workspace` and + `update-workspace`. +properties: + id: + type: string + description: | + Workspace identifier. Required for all workspace operations. + Immutable after creation. + examples: + - default + - production + name: + type: string + description: Human-readable workspace name. + examples: + - Default Workspace + - Production + enabled: + type: boolean + description: Whether the workspace is enabled. + default: true diff --git a/specs/api/components/schemas/iam/WorkspaceRecord.yaml b/specs/api/components/schemas/iam/WorkspaceRecord.yaml new file mode 100644 index 00000000..ff9026f8 --- /dev/null +++ b/specs/api/components/schemas/iam/WorkspaceRecord.yaml @@ -0,0 +1,21 @@ +type: object +description: Workspace record returned by IAM operations. +properties: + id: + type: string + description: Workspace identifier. + examples: + - default + name: + type: string + description: Human-readable workspace name. + examples: + - Default Workspace + enabled: + type: boolean + description: Whether the workspace is enabled. + created: + type: string + description: Creation timestamp (ISO-8601 UTC). + examples: + - "2026-01-01T00:00:00Z" diff --git a/specs/api/openapi.yaml b/specs/api/openapi.yaml index 5d1726fe..45540365 100644 --- a/specs/api/openapi.yaml +++ b/specs/api/openapi.yaml @@ -89,6 +89,8 @@ security: - bearerAuth: [] tags: + - name: IAM + description: Identity and access management (global) - name: Config description: Configuration management (workspace-scoped) - name: Flow @@ -109,6 +111,11 @@ tags: description: System metrics and monitoring paths: + # Global services + /api/v1/iam: + $ref: './paths/iam.yaml' + + # Workspace-scoped services /api/v1/config: $ref: './paths/config.yaml' /api/v1/flow: diff --git a/specs/api/paths/iam.yaml b/specs/api/paths/iam.yaml new file mode 100644 index 00000000..e1a94dc5 --- /dev/null +++ b/specs/api/paths/iam.yaml @@ -0,0 +1,206 @@ +post: + tags: + - IAM + summary: IAM service (global) + description: | + Identity and access management service. + + This is a **global service** — it operates at system level, not + scoped to a specific workspace. The `workspace` field in the + request body is used as a scope filter or integrity check on + certain operations, not as an addressing component. + + ## Authentication + + Most operations require a bearer token. The gateway resolves the + token to an authenticated identity and injects the `actor` field + (the caller's user ID) into the request. Clients cannot set + `actor` — the gateway overwrites it. + + ## Operations by Capability + + ### Any authenticated user + - `whoami`: Return the caller's own user record + - `list-my-workspaces`: List workspaces the caller has access to. + For open-source IAM: returns the caller's home workspace, or all + workspaces if the caller has the `admin` role. + + ### User management (`users:read` / `users:write` / `users:admin`) + - `create-user`: Create a new user in a workspace + - `list-users`: List users, optionally filtered by workspace + - `get-user`: Get a user record by ID + - `update-user`: Update user fields (name, email, roles, enabled) + - `disable-user`: Soft-disable a user and revoke their API keys + - `enable-user`: Re-enable a disabled user + - `delete-user`: Hard-delete a user and their API keys + + ### Workspace management (`workspaces:admin`) + - `create-workspace`: Create a new workspace + - `list-workspaces`: List all workspaces (admin view) + - `get-workspace`: Get a workspace record + - `update-workspace`: Update workspace name or enabled state + - `disable-workspace`: Disable a workspace and all its users + + ### API key management (`keys:self` / `keys:admin`) + - `create-api-key`: Create an API key (plaintext returned once) + - `list-api-keys`: List API keys for a user + - `revoke-api-key`: Revoke (delete) an API key + + ### Password management (`users:admin`) + - `reset-password`: Admin-initiated password reset (returns temporary password) + + ### System (`iam:admin`) + - `rotate-signing-key`: Rotate the JWT signing key + + operationId: iamService + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '../components/schemas/iam/IamRequest.yaml' + examples: + whoami: + summary: Get the caller's own user record + value: + operation: whoami + listMyWorkspaces: + summary: List workspaces the caller has access to + value: + operation: list-my-workspaces + createUser: + summary: Create a new user + value: + operation: create-user + workspace: default + user: + username: alice + name: Alice Smith + email: alice@example.com + password: changeme123 + roles: + - writer + listUsers: + summary: List users in a workspace + value: + operation: list-users + workspace: default + getUser: + summary: Get a specific user + value: + operation: get-user + user_id: usr_abc123 + updateUser: + summary: Update a user's roles + value: + operation: update-user + user_id: usr_abc123 + user: + roles: + - admin + disableUser: + summary: Disable a user + value: + operation: disable-user + user_id: usr_abc123 + createWorkspace: + summary: Create a workspace + value: + operation: create-workspace + workspace_record: + id: production + name: Production Workspace + listWorkspaces: + summary: List all workspaces (admin) + value: + operation: list-workspaces + createApiKey: + summary: Create an API key + value: + operation: create-api-key + key: + user_id: usr_abc123 + name: laptop + expires: "2027-01-01T00:00:00Z" + listApiKeys: + summary: List a user's API keys + value: + operation: list-api-keys + user_id: usr_abc123 + revokeApiKey: + summary: Revoke an API key + value: + operation: revoke-api-key + key_id: key_xyz789 + resetPassword: + summary: Admin-initiated password reset + value: + operation: reset-password + user_id: usr_abc123 + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '../components/schemas/iam/IamResponse.yaml' + examples: + whoami: + summary: Caller's user record + value: + user: + id: usr_abc123 + workspace: default + username: alice + name: Alice Smith + email: alice@example.com + roles: + - writer + enabled: true + must_change_password: false + created: "2026-01-15T10:30:00Z" + listMyWorkspaces: + summary: Workspaces the caller can access + value: + workspaces: + - id: default + name: Default Workspace + enabled: true + created: "2026-01-01T00:00:00Z" + listUsers: + summary: Users in a workspace + value: + users: + - id: usr_abc123 + workspace: default + username: alice + name: Alice Smith + roles: + - writer + enabled: true + created: "2026-01-15T10:30:00Z" + createApiKey: + summary: New API key (plaintext returned once) + value: + api_key_plaintext: tg_aBcDeFgHiJkLmNoPqRsTuVwXyZ + api_key: + id: key_xyz789 + user_id: usr_abc123 + name: laptop + prefix: tg_a + expires: "2027-01-01T00:00:00Z" + created: "2026-05-29T14:00:00Z" + resetPassword: + summary: Temporary password (returned once) + value: + temporary_password: tmp_xK9mQ2pL + '400': + description: Bad request (unknown operation, missing required fields) + '401': + $ref: '../components/responses/Unauthorized.yaml' + '403': + description: Access denied (insufficient capabilities) + '500': + $ref: '../components/responses/Error.yaml' diff --git a/specs/websocket/components/messages/ServiceRequest.yaml b/specs/websocket/components/messages/ServiceRequest.yaml index 7619a250..ad7f3508 100644 --- a/specs/websocket/components/messages/ServiceRequest.yaml +++ b/specs/websocket/components/messages/ServiceRequest.yaml @@ -9,6 +9,9 @@ description: | payload: description: Service request envelope with id, service, optional flow, and service-specific request payload oneOf: + # Global services + - $ref: './requests/IamRequest.yaml' + # Workspace-scoped services (no flow parameter) - $ref: './requests/ConfigRequest.yaml' - $ref: './requests/FlowRequest.yaml' diff --git a/specs/websocket/components/messages/requests/IamRequest.yaml b/specs/websocket/components/messages/requests/IamRequest.yaml new file mode 100644 index 00000000..4211552d --- /dev/null +++ b/specs/websocket/components/messages/requests/IamRequest.yaml @@ -0,0 +1,25 @@ +type: object +description: WebSocket request for IAM service (global service) +required: + - id + - service + - request +properties: + id: + type: string + description: Unique request identifier + service: + type: string + const: iam + description: Service identifier for IAM service + request: + $ref: '../../../../api/components/schemas/iam/IamRequest.yaml' +examples: + - id: req-1 + service: iam + request: + operation: whoami + - id: req-2 + service: iam + request: + operation: list-my-workspaces diff --git a/trustgraph-base/trustgraph/base/iam_client.py b/trustgraph-base/trustgraph/base/iam_client.py index e0457d19..a2878a0a 100644 --- a/trustgraph-base/trustgraph/base/iam_client.py +++ b/trustgraph-base/trustgraph/base/iam_client.py @@ -300,6 +300,14 @@ class IamClient(RequestResponse): ) return resp.workspace + async def list_my_workspaces(self, actor="", timeout=IAM_TIMEOUT): + resp = await self._request( + operation="list-my-workspaces", + actor=actor, + timeout=timeout, + ) + return list(resp.workspaces) + async def list_workspaces(self, actor="", timeout=IAM_TIMEOUT): resp = await self._request( operation="list-workspaces", diff --git a/trustgraph-cli/pyproject.toml b/trustgraph-cli/pyproject.toml index fafcc9bc..16b0ae0a 100644 --- a/trustgraph-cli/pyproject.toml +++ b/trustgraph-cli/pyproject.toml @@ -56,6 +56,7 @@ tg-create-api-key = "trustgraph.cli.create_api_key:main" tg-list-api-keys = "trustgraph.cli.list_api_keys:main" tg-revoke-api-key = "trustgraph.cli.revoke_api_key:main" tg-list-workspaces = "trustgraph.cli.list_workspaces:main" +tg-list-my-workspaces = "trustgraph.cli.list_my_workspaces:main" tg-create-workspace = "trustgraph.cli.create_workspace:main" tg-invoke-agent = "trustgraph.cli.invoke_agent:main" tg-invoke-document-rag = "trustgraph.cli.invoke_document_rag:main" diff --git a/trustgraph-cli/trustgraph/cli/list_my_workspaces.py b/trustgraph-cli/trustgraph/cli/list_my_workspaces.py new file mode 100644 index 00000000..e0d3a53b --- /dev/null +++ b/trustgraph-cli/trustgraph/cli/list_my_workspaces.py @@ -0,0 +1,53 @@ +""" +List workspaces the current user has access to. +""" + +import argparse + +import tabulate + +from ._iam import DEFAULT_URL, DEFAULT_TOKEN, call_iam, run_main + + +def do_list_my_workspaces(args): + resp = call_iam( + args.api_url, args.token, {"operation": "list-my-workspaces"}, + ) + workspaces = resp.get("workspaces", []) + if not workspaces: + print("No workspaces.") + return + rows = [ + [ + w.get("id", ""), + w.get("name", ""), + "yes" if w.get("enabled") else "no", + w.get("created", ""), + ] + for w in workspaces + ] + print(tabulate.tabulate( + rows, + headers=["id", "name", "enabled", "created"], + tablefmt="pretty", + stralign="left", + )) + + +def main(): + parser = argparse.ArgumentParser( + prog="tg-list-my-workspaces", description=__doc__, + ) + parser.add_argument( + "-u", "--api-url", default=DEFAULT_URL, + help=f"API URL (default: {DEFAULT_URL})", + ) + parser.add_argument( + "-t", "--token", default=DEFAULT_TOKEN, + help="Auth token (default: $TRUSTGRAPH_TOKEN)", + ) + run_main(do_list_my_workspaces, parser) + + +if __name__ == "__main__": + main() diff --git a/trustgraph-flow/trustgraph/gateway/registry.py b/trustgraph-flow/trustgraph/gateway/registry.py index 4d439097..ca235315 100644 --- a/trustgraph-flow/trustgraph/gateway/registry.py +++ b/trustgraph-flow/trustgraph/gateway/registry.py @@ -309,6 +309,13 @@ register(Operation( extract_resource=_empty_resource, extract_parameters=_no_parameters, )) +register(Operation( + name="list-my-workspaces", + capability="workspaces:list-own", + resource_level=ResourceLevel.SYSTEM, + extract_resource=_empty_resource, + extract_parameters=_no_parameters, +)) register(Operation( name="list-workspaces", capability="workspaces:admin", diff --git a/trustgraph-flow/trustgraph/iam/noauth/handler.py b/trustgraph-flow/trustgraph/iam/noauth/handler.py index d457697e..dd70b02d 100644 --- a/trustgraph-flow/trustgraph/iam/noauth/handler.py +++ b/trustgraph-flow/trustgraph/iam/noauth/handler.py @@ -106,7 +106,7 @@ class NoAuthHandler: ): return IamResponse() - if op == "list-workspaces": + if op in ("list-workspaces", "list-my-workspaces"): return IamResponse() if op in ("create-api-key", "list-api-keys", "revoke-api-key"): diff --git a/trustgraph-flow/trustgraph/iam/service/iam.py b/trustgraph-flow/trustgraph/iam/service/iam.py index 0335012e..5f86e688 100644 --- a/trustgraph-flow/trustgraph/iam/service/iam.py +++ b/trustgraph-flow/trustgraph/iam/service/iam.py @@ -68,6 +68,7 @@ _READER_CAPS = { "collections:read", "knowledge:read", "keys:self", + "workspaces:list-own", } _WRITER_CAPS = _READER_CAPS | { @@ -328,6 +329,8 @@ class IamService: return await self.handle_delete_user(v) if op == "create-workspace": return await self.handle_create_workspace(v) + if op == "list-my-workspaces": + return await self.handle_list_my_workspaces(v) if op == "list-workspaces": return await self.handle_list_workspaces(v) if op == "get-workspace": @@ -915,6 +918,30 @@ class IamService: row = await self.table_store.get_workspace(v.workspace_record.id) return IamResponse(workspace=self._row_to_workspace_record(row)) + async def handle_list_my_workspaces(self, v): + if not v.actor: + return _err("invalid-argument", "actor required") + + user_row = await self.table_store.get_user(v.actor) + if user_row is None: + return _err("not-found", "user not found") + + user_roles = user_row[6] or [] + is_admin = "admin" in user_roles + + if is_admin: + rows = await self.table_store.list_workspaces() + else: + user_workspace = user_row[1] + row = await self.table_store.get_workspace(user_workspace) + rows = [row] if row else [] + + return IamResponse( + workspaces=[ + self._row_to_workspace_record(r) for r in rows + ], + ) + async def handle_list_workspaces(self, v): rows = await self.table_store.list_workspaces() return IamResponse(