410 lines
20 KiB
JSON
410 lines
20 KiB
JSON
{
|
|
"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"}}
|
|
}
|
|
}
|
|
}
|
|
}
|