{ "openapi": "3.1.0", "info": { "title": "Go Loose API", "version": "0.1.0", "description": "Multi-tenant API-key management and runtime authorization." }, "servers": [{"url": "/"}], "components": { "securitySchemes": { "session": {"type": "apiKey", "in": "cookie", "name": "go_loose_session"}, "apiKey": {"type": "apiKey", "in": "header", "name": "X-API-Key"}, "clientBasic": {"type": "http", "scheme": "basic"}, "userBearer": {"type": "http", "scheme": "bearer"} }, "schemas": { "Error": {"type": "object", "properties": {"error": {"type": "string"}}}, "AuthorizationRequest": { "type": "object", "required": ["application", "method", "path"], "properties": { "tenant": {"type": "string", "example": "nmbs"}, "application": {"type": "string", "example": "guess"}, "method": {"type": "string", "example": "GET"}, "path": {"type": "string", "example": "/api/guesses"}, "host": {"type": "string", "example": "nmbs.guess.local"} } }, "AuthorizationResponse": { "type": "object", "properties": { "allowed": {"type": "boolean"}, "reason": {"type": "string"}, "tenant_id": {"type": "string", "format": "uuid"}, "tenant_slug": {"type": "string"}, "application_id": {"type": "string", "format": "uuid"}, "application_slug": {"type": "string"}, "api_key_id": {"type": "string", "format": "uuid"}, "api_key_name": {"type": "string"} } }, "Membership": { "type": "object", "description": "A tenant role held by a user, with the capabilities it grants.", "properties": { "tenant_id": {"type": "string", "format": "uuid"}, "tenant_slug": {"type": "string"}, "tenant_name": {"type": "string"}, "role": {"type": "string", "description": "Slug of the role, which may be a role this tenant defined."}, "role_id": {"type": "string", "format": "uuid", "description": "Identifier of the role row. Send this back when assigning the role."}, "permissions": {"type": "array", "items": {"$ref": "#/components/schemas/PermissionValue"}} } }, "Profile": { "type": "object", "properties": { "id": {"type": "string", "format": "uuid"}, "email": {"type": "string", "format": "email"}, "display_name": {"type": "string"}, "avatar_url": {"type": "string", "description": "Absolute picture URL, or an empty string when no picture is set."}, "memberships": {"type": "array", "items": {"$ref": "#/components/schemas/Membership"}}, "is_system_administrator": {"type": "boolean"}, "name_customized": {"type": "boolean", "description": "True once the user set the display name themselves, which stops the identity provider from overwriting it."}, "created_at": {"type": "string", "format": "date-time"}, "last_login_at": {"type": "string", "format": "date-time"}, "profile_updated_at": {"type": "string", "format": "date-time"}, "avatar_updated_at": {"type": "string", "format": "date-time"} } }, "ProfileUpdate": { "type": "object", "description": "Send display_name, avatar, or remove_avatar. avatar accepts raw base64 or a data URL.", "properties": { "display_name": {"type": "string", "minLength": 1, "maxLength": 200}, "avatar": {"type": "string", "description": "Base64 or data URL of a JPEG, PNG, or GIF up to 2 MiB and 4096 by 4096 pixels."}, "remove_avatar": {"type": "boolean"} } }, "Role": { "type": "object", "description": "A named bundle of permissions. A built-in role has no tenant_id, is visible to every tenant, and cannot be edited or deleted. A tenant may define its own.", "required": ["id", "slug", "name", "system", "permissions"], "properties": { "id": {"type": "string", "format": "uuid"}, "tenant_id": {"type": "string", "format": "uuid", "description": "Absent for built-in roles."}, "slug": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"}, "name": {"type": "string", "minLength": 1, "maxLength": 100}, "description": {"type": "string", "maxLength": 500}, "system": {"type": "boolean", "description": "True for built-in roles, which are read only."}, "permissions": {"type": "array", "items": {"$ref": "#/components/schemas/PermissionValue"}}, "member_count": {"type": "integer", "description": "Members currently holding the role. A role that still has members cannot be deleted."}, "assignable": {"type": "boolean", "description": "Whether the caller may assign this role. False when the role grants a permission the caller does not hold, which is what stops role assignment becoming a way to gain authority."} } }, "PermissionValue": { "type": "string", "description": "The closed vocabulary of capabilities a role may grant. Mirrors internal/role and the role_permissions check constraint.", "enum": ["tenants.manage", "roles.manage", "users.manage", "applications.manage", "keys.manage", "contracts.manage", "users.read", "console.read", "profile.edit"] }, "PermissionEntry": { "type": "object", "description": "A permission with the label and description the console renders.", "properties": { "value": {"$ref": "#/components/schemas/PermissionValue"}, "label": {"type": "string"}, "description": {"type": "string"} } }, "RoleRequest": { "type": "object", "required": ["name", "permissions"], "properties": { "tenant_id": {"type": "string", "format": "uuid", "description": "Required when creating. Ignored when updating, which takes the role's own tenant."}, "slug": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,62}$", "description": "Required when creating, and fixed for the life of the role."}, "name": {"type": "string", "minLength": 1, "maxLength": 100}, "description": {"type": "string", "maxLength": 500}, "permissions": {"type": "array", "items": {"$ref": "#/components/schemas/PermissionValue"}} } } } }, "paths": { "/healthz": { "get": { "tags": [ "Monitoring" ], "summary": "Liveness check", "responses": {"200": {"description": "Healthy"}} } }, "/install": { "get": { "tags": ["Installation"], "summary": "Show the first-run installer", "responses": {"200": {"description": "Installer form"}, "303": {"description": "Already installed"}} }, "post": { "tags": ["Installation"], "summary": "Persist Google OIDC configuration and optional demo data", "requestBody": {"required": true, "content": {"application/x-www-form-urlencoded": {"schema": {"type": "object", "required": ["csrf_token", "oidc_client_id", "oidc_client_secret"], "properties": {"csrf_token": {"type": "string"}, "oidc_client_id": {"type": "string"}, "oidc_client_secret": {"type": "string", "format": "password"}, "seed_demo": {"type": "boolean"}}}}}}, "responses": {"303": {"description": "Installation completed; continue to Google OIDC login"}, "409": {"description": "Already installed"}, "502": {"description": "Google OIDC discovery failed"}} } }, "/auth/password": { "post": { "tags": ["User Action"], "summary": "Authenticate a database-backed user", "requestBody": {"required": true, "content": {"application/x-www-form-urlencoded": {"schema": {"type": "object", "required": ["email", "password"], "properties": {"email": {"type": "string", "format": "email"}, "password": {"type": "string", "format": "password"}, "return": {"type": "string", "format": "uri"}}}}}}, "responses": {"303": {"description": "Login succeeded"}, "401": {"description": "Invalid email or password"}, "429": {"description": "Temporarily rate limited"}} } }, "/api/v1/authorize": { "post": { "tags": [ "Authorization Management" ], "summary": "Authorize an incoming API request", "security": [{"apiKey": []}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthorizationRequest"}}}}, "responses": { "200": {"description": "Authorized", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthorizationResponse"}}}}, "401": {"description": "Missing or invalid key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "403": {"description": "Key not allowed for this request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthorizationResponse"}}}} } } }, "/api/v1/dashboard": { "get": { "tags": [ "Dashboard Management" ], "summary": "Get management console data", "description": "Requires a role that grants console.read. Each tenant and user entry carries the capability names its role grants.", "security": [{"session": []}], "responses": {"200": {"description": "Dashboard data"}} } }, "/profile": { "get": { "tags": ["Profile"], "summary": "Open the self-service profile screen", "description": "Available to every authenticated management session, including self-service User members.", "security": [{"session": []}], "responses": {"200": {"description": "Profile screen"}, "303": {"description": "Redirect to login"}} } }, "/api/v1/profile": { "get": { "tags": ["Profile"], "summary": "Read the caller's own profile and tenant roles", "security": [{"session": []}], "responses": { "200": {"description": "Profile", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Profile"}}}}, "401": {"description": "Authentication required"} } }, "put": { "tags": ["Profile"], "summary": "Update the caller's display name or profile picture", "security": [{"session": []}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProfileUpdate"}}}}, "responses": { "200": {"description": "Updated profile", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Profile"}}}}, "400": {"description": "Invalid display name or unsupported picture"} } } }, "/api/v1/avatars/{key}": { "get": { "tags": ["Profile"], "summary": "Fetch a profile picture", "description": "Public on purpose so other applications can embed the picture with a plain image tag. The key is 256 bits of randomness and exposes nothing about the user.", "parameters": [{"in": "path", "name": "key", "required": true, "schema": {"type": "string"}}], "responses": { "200": {"description": "Image bytes", "content": {"image/jpeg": {"schema": {"type": "string", "format": "binary"}}, "image/png": {"schema": {"type": "string", "format": "binary"}}, "image/gif": {"schema": {"type": "string", "format": "binary"}}}}, "304": {"description": "Not modified"}, "404": {"description": "No picture for that key"} } } }, "/api/v1/tenants": { "post": { "tags": [ "Tenant Management" ], "summary": "Create a tenant", "security": [{"session": []}], "responses": {"201": {"description": "Tenant created"}} } }, "/api/v1/applications": { "post": { "tags": [ "Application Management" ], "summary": "Register an application", "security": [{"session": []}], "responses": {"201": {"description": "Application registered"}} } }, "/api/v1/keys": { "post": { "tags": [ "Key Management" ], "summary": "Issue an API key", "security": [{"session": []}], "responses": {"201": {"description": "Key created; plaintext secret is returned once"}} } }, "/api/v1/keys/{id}/revoke": { "post": { "tags": [ "Key Management" ], "summary": "Revoke an API key", "security": [{"session": []}], "parameters": [{"in": "path", "name": "id", "required": true, "schema": {"type": "string", "format": "uuid"}}], "responses": {"204": {"description": "Key revoked"}} } }, "/api/v1/contracts": { "post": { "tags": [ "Contract Management" ], "summary": "Import a pasted contract or scan an OpenAPI URL", "security": [{"session": []}], "responses": {"201": {"description": "Contract imported"}} } }, "/connect/authorize": { "get": { "tags": [ "User Action" ], "summary": "Start hosted client application login", "parameters": [ {"in": "query", "name": "response_type", "required": true, "schema": {"type": "string", "const": "code"}}, {"in": "query", "name": "client_id", "required": true, "schema": {"type": "string"}}, {"in": "query", "name": "redirect_uri", "required": true, "schema": {"type": "string", "format": "uri"}}, {"in": "query", "name": "state", "required": true, "schema": {"type": "string"}} ], "responses": {"302": {"description": "Redirect to login or exact client callback"}} } }, "/connect/token": { "post": { "tags": [ "User Action" ], "summary": "Exchange a one-time authorization code", "description": "The nested user object carries role, permissions, and avatar_url for the tenant owning the application.", "security": [{"clientBasic": []}], "requestBody": {"required": true, "content": {"application/x-www-form-urlencoded": {"schema": {"type": "object"}}}}, "responses": {"200": {"description": "Opaque client user session"}, "401": {"description": "Invalid code or client credentials"}} } }, "/connect/userinfo": { "get": { "tags": [ "User Action" ], "summary": "Validate a client user session and read the caller's role and picture", "description": "Returns id, email, display_name, avatar_url, tenant and application identity, role, and permissions.", "security": [{"userBearer": []}], "responses": {"200": {"description": "Authenticated client user"}, "401": {"description": "Invalid, revoked, or expired session"}} } }, "/connect/logout": { "post": { "tags": [ "User Action" ], "summary": "Revoke a client user session", "security": [{"userBearer": []}], "responses": {"204": {"description": "Session revoked"}} } }, "/api/v1/applications/{id}/client-config": { "post": { "tags": [ "Application Management" ], "summary": "Configure exact redirect URIs and rotate the client secret", "security": [{"session": []}], "parameters": [{"in": "path", "name": "id", "required": true, "schema": {"type": "string", "format": "uuid"}}], "responses": {"200": {"description": "Configuration saved; client secret returned once"}} } }, "/api/v1/users": { "post": { "tags": [ "User Management" ], "summary": "Invite a tenant user with a role", "description": "Requires users.manage in the tenant, which owner, admin, developer, and operator hold. role_id must name a role of that tenant whose permissions are a subset of the caller's own.", "security": [{"session": []}], "responses": {"201": {"description": "User invited"}} } }, "/api/v1/users/{id}/access": { "put": { "tags": [ "User Management" ], "summary": "Set tenant role and application access", "description": "Requires users.manage. The caller may not grant a role more powerful than their own, and may not demote the last owner of a tenant.", "security": [{"session": []}], "parameters": [{"in": "path", "name": "id", "required": true, "schema": {"type": "string", "format": "uuid"}}], "responses": {"204": {"description": "Access updated"}} } }, "/api/v1/roles": { "get": { "tags": ["Role Management"], "summary": "List the roles available in a tenant", "description": "Returns every built-in role plus the roles this tenant defined, each annotated with whether the caller may assign it.", "security": [{"session": []}], "parameters": [{"in": "query", "name": "tenant_id", "required": true, "schema": {"type": "string", "format": "uuid"}}], "responses": {"200": {"description": "Role catalog", "content": {"application/json": {"schema": {"type": "object", "properties": {"roles": {"type": "array", "items": {"$ref": "#/components/schemas/Role"}}}}}}}, "404": {"description": "Not a member of that tenant"}} }, "post": { "tags": ["Role Management"], "summary": "Define a role for a tenant", "description": "Requires roles.manage in the tenant. The role may not grant a permission the caller does not already hold, so an administrator cannot mint their way above their own authority.", "security": [{"session": []}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/RoleRequest"}}}}, "responses": {"201": {"description": "Role created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Role"}}}}, "400": {"description": "Malformed slug or name, or an unknown permission"}, "404": {"description": "The caller does not hold roles.manage, or the role would exceed their authority"}, "409": {"description": "The slug is already used in this tenant"}} } }, "/api/v1/roles/{id}": { "put": { "tags": ["Role Management"], "summary": "Rename a role and replace its permissions", "description": "Built-in roles are immutable. Saving replaces the permissions of every member holding the role, so treat it as a live change.", "security": [{"session": []}], "parameters": [{"in": "path", "name": "id", "required": true, "schema": {"type": "string", "format": "uuid"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/RoleRequest"}}}}, "responses": {"200": {"description": "Role updated", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Role"}}}}, "400": {"description": "Malformed name or an unknown permission"}, "404": {"description": "Unknown role, a built-in role, or a role belonging to another tenant"}} }, "delete": { "tags": ["Role Management"], "summary": "Delete one of the tenant's own roles", "security": [{"session": []}], "parameters": [{"in": "path", "name": "id", "required": true, "schema": {"type": "string", "format": "uuid"}}], "responses": {"204": {"description": "Role deleted"}, "404": {"description": "Unknown role, a built-in role, or a role belonging to another tenant"}, "409": {"description": "The role still has members"}} } }, "/roles": { "get": { "tags": ["Role Management"], "summary": "Role management screen", "security": [{"session": []}], "responses": {"200": {"description": "HTML page"}} } }, "/api/v1/users/{id}/approve": { "post": { "tags": ["User Management"], "summary": "Approve a waiting-room user into a tenant", "description": "Requires a system administrator session. Unlike the tenant-scoped endpoints this is not limited by the caller's own role, because it grants a first membership across tenants.", "security": [{"session": []}], "parameters": [{"in": "path", "name": "id", "required": true, "schema": {"type": "string", "format": "uuid"}}], "responses": {"204": {"description": "User approved"}, "404": {"description": "User or permission not found"}} } } } }