Merged in feature/admin-api (pull request #191)

admin api and associated tools

* in progress

admin api started and permit.io setup tool started

* docs

* build fix

* integration tests passing

* new permit variables

* fix permit

* fix delete regex

* fix delete response

* docs

* docs
This commit is contained in:
Jay Brown
2025-10-23 22:57:15 +00:00
parent 7638fd3a90
commit 2b43799f56
29 changed files with 10579 additions and 165 deletions
+822
View File
@@ -27,6 +27,8 @@ tags:
description: Operations related to exports
- name: AuthService
description: Operations related to authentication
- name: AdminService
description: Operations related to user management and administration
paths:
@@ -841,6 +843,452 @@ paths:
"500":
$ref: "#/components/responses/InternalError"
/admin/users:
post:
operationId: createAdminUser
tags:
- AdminService
summary: Create a new user
description: >-
Creates a new user in both AWS Cognito and Permit.io systems with optional role assignments.
Idempotent - returns 200 OK if user already exists with identical attributes.
**Creation Flow:**
1. User is created in AWS Cognito (primary authentication system)
2. If Cognito creation fails, operation fails with HTTP 502 (no user created)
3. User is created in Permit.io (authorization/role system)
4. If Permit.io creation fails, user still exists in Cognito but permit_synced=false
5. Roles are assigned in Permit.io (best-effort, continues on failures)
**Role Assignment Behavior:**
- Roles must exist in your Permit.io environment (e.g., super_admin, user_admin, auditor, client_user)
- Invalid/non-existent roles fail silently and are logged server-side
- Only successfully assigned roles appear in the roles_assigned response field
- The roles_assigned array may be a subset of the requested roles array
- If all roles fail, roles_assigned will be null or empty (user has no permissions)
**Partial Success Scenarios:**
- Returns HTTP 201 even if Permit.io creation or some role assignments fail
- Check permit_synced field: false indicates user was not created in Permit.io
- Compare roles_assigned vs requested roles to detect role assignment failures
- User can authenticate (Cognito) but may lack authorization (Permit.io) if sync fails
security:
- jwtAuth: []
requestBody:
description: The user details required to create a user
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AdminUserCreate"
responses:
"201":
description: User created successfully.
headers:
RateLimit:
$ref: "#/components/headers/RateLimit"
content:
application/json:
schema:
$ref: "#/components/schemas/AdminUserCreateResponse"
"200":
description: User already exists with matching attributes (idempotent).
headers:
RateLimit:
$ref: "#/components/headers/RateLimit"
content:
application/json:
schema:
$ref: "#/components/schemas/AdminUserCreateResponse"
"400":
$ref: "#/components/responses/InvalidRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"409":
$ref: "#/components/responses/Conflict"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/InternalError"
"502":
$ref: "#/components/responses/BadGateway"
get:
operationId: listAdminUsers
tags:
- AdminService
summary: List users
description: >-
Lists users from AWS Cognito with pagination, filtering, and sorting support.
security:
- jwtAuth: []
parameters:
- name: page
in: query
description: Page number for pagination
schema:
type: integer
format: int32
minimum: 1
default: 1
maximum: 10000
- name: page_size
in: query
description: Number of results per page
schema:
type: integer
format: int32
minimum: 1
maximum: 100
default: 50
- name: search
in: query
description: Search term for email/name filtering
schema:
type: string
maxLength: 256
pattern: "^.+$"
- name: status
in: query
description: Filter by user status
schema:
type: string
enum: [enabled, disabled, all]
default: all
- name: sort_by
in: query
description: Field to sort by
schema:
type: string
enum: [email, created_at, last_name]
default: email
- name: sort_order
in: query
description: Sort direction
schema:
type: string
enum: [asc, desc]
default: asc
responses:
"200":
description: List of users.
headers:
RateLimit:
$ref: "#/components/headers/RateLimit"
content:
application/json:
schema:
$ref: "#/components/schemas/AdminUserList"
"400":
$ref: "#/components/responses/InvalidRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/InternalError"
/admin/users/{email}:
parameters:
- $ref: "#/components/parameters/UserEmail"
get:
operationId: getAdminUser
tags:
- AdminService
summary: Get user by email
description: >-
Retrieves user details from both AWS Cognito and Permit.io by email address.
security:
- jwtAuth: []
responses:
"200":
description: User details.
headers:
RateLimit:
$ref: "#/components/headers/RateLimit"
content:
application/json:
schema:
$ref: "#/components/schemas/AdminUserDetails"
"400":
$ref: "#/components/responses/InvalidRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/InternalError"
head:
operationId: checkAdminUserExists
tags:
- AdminService
summary: Check if user exists
description: >-
Checks if a user exists in Cognito and/or Permit.io without returning full details.
security:
- jwtAuth: []
responses:
"200":
description: User exists.
headers:
RateLimit:
$ref: "#/components/headers/RateLimit"
X-User-Exists-Cognito:
schema:
type: boolean
description: Whether user exists in Cognito
X-User-Exists-PermitIO:
schema:
type: boolean
description: Whether user exists in Permit.io
"400":
$ref: "#/components/responses/InvalidRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
description: User does not exist in either system
headers:
RateLimit:
$ref: "#/components/headers/RateLimit"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/InternalError"
patch:
operationId: updateAdminUser
tags:
- AdminService
summary: Update user attributes and roles
description: >-
Updates user attributes (first_name, last_name) in Cognito and manages role assignments in Permit.io.
Email address cannot be changed (use email path parameter to identify user).
**Attribute Updates:**
- first_name and last_name are updated in AWS Cognito
- Omit fields you don't want to change (e.g., send only roles to change roles)
**Role Management (REPLACE Strategy):**
- Providing a roles array REPLACES all existing roles (not additive)
- To add a role: include ALL current roles PLUS the new role
- To remove a role: include only the roles you want to keep
- To remove all roles: send an empty array []
- Omitting the roles field leaves role assignments unchanged
**Role Assignment Behavior:**
- Roles must exist in your Permit.io environment
- Invalid/non-existent roles fail silently and are logged server-side
- Response.roles field shows the final state after successful assignments
- Compare response.roles vs requested roles to detect failures
**Example - Add a Role:**
Current roles: ["client_user"]
To add "auditor": send roles: ["client_user", "auditor"]
Result: ["client_user", "auditor"]
**Example - Replace All Roles:**
Current roles: ["auditor", "client_user"]
To change to just "super_admin": send roles: ["super_admin"]
Result: ["super_admin"] (auditor and client_user removed)
security:
- jwtAuth: []
requestBody:
description: The user attributes to update
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AdminUserUpdate"
responses:
"200":
description: User updated successfully.
headers:
RateLimit:
$ref: "#/components/headers/RateLimit"
content:
application/json:
schema:
$ref: "#/components/schemas/AdminUserDetails"
"400":
$ref: "#/components/responses/InvalidRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/InternalError"
delete:
operationId: deleteAdminUser
tags:
- AdminService
summary: Delete user permanently
description: >-
Permanently deletes a user from both AWS Cognito and Permit.io.
This operation is irreversible. Requires confirm=true query parameter.
security:
- jwtAuth: []
parameters:
- name: confirm
in: query
description: Must be set to true to confirm deletion
required: true
schema:
type: boolean
responses:
"200":
description: User deleted successfully.
headers:
RateLimit:
$ref: "#/components/headers/RateLimit"
content:
application/json:
schema:
$ref: "#/components/schemas/AdminUserDeleteResponse"
"207":
description: User partially deleted (multi-status).
headers:
RateLimit:
$ref: "#/components/headers/RateLimit"
content:
application/json:
schema:
$ref: "#/components/schemas/AdminUserDeleteResponse"
"400":
$ref: "#/components/responses/InvalidRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/InternalError"
/admin/users/{email}/disable:
parameters:
- $ref: "#/components/parameters/UserEmail"
post:
operationId: disableAdminUser
tags:
- AdminService
summary: Disable user
description: >-
Disables a user in AWS Cognito, preventing authentication.
User data and roles are preserved. Idempotent.
security:
- jwtAuth: []
responses:
"200":
description: User disabled successfully.
headers:
RateLimit:
$ref: "#/components/headers/RateLimit"
content:
application/json:
schema:
$ref: "#/components/schemas/AdminUserActionResponse"
"400":
$ref: "#/components/responses/InvalidRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/InternalError"
/admin/users/{email}/enable:
parameters:
- $ref: "#/components/parameters/UserEmail"
post:
operationId: enableAdminUser
tags:
- AdminService
summary: Enable user
description: >-
Re-enables a previously disabled user in AWS Cognito.
Restores authentication capability. Idempotent.
security:
- jwtAuth: []
responses:
"200":
description: User enabled successfully.
headers:
RateLimit:
$ref: "#/components/headers/RateLimit"
content:
application/json:
schema:
$ref: "#/components/schemas/AdminUserActionResponse"
"400":
$ref: "#/components/responses/InvalidRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/InternalError"
components:
parameters:
ClientID:
@@ -883,6 +1331,16 @@ components:
$ref: "#/components/schemas/BatchID"
description: The batch upload ID.
UserEmail:
in: path
name: email
required: true
schema:
type: string
format: email
maxLength: 320
description: URL-encoded user email address.
headers:
RateLimit:
schema:
@@ -976,6 +1434,36 @@ components:
schema:
$ref: "#/components/schemas/ErrorMessage"
Forbidden:
description: Insufficient permissions to perform this action.
headers:
RateLimit:
$ref: "#/components/headers/RateLimit"
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorMessage"
Conflict:
description: Resource already exists or state conflict.
headers:
RateLimit:
$ref: "#/components/headers/RateLimit"
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorMessage"
BadGateway:
description: External service error.
headers:
RateLimit:
$ref: "#/components/headers/RateLimit"
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorMessage"
schemas:
# Query API schemas
ClientID:
@@ -1563,3 +2051,337 @@ components:
- message
example:
message: you must include a message
# Admin API schemas
AdminUserCreate:
description: Request body for creating a new user with email, name, and role assignments
type: object
required:
- email
- first_name
- last_name
- roles
properties:
email:
type: string
format: email
maxLength: 320
pattern: '^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$'
description: User email address (used as Cognito username)
example: john.doe@example.com
first_name:
type: string
minLength: 1
maxLength: 100
pattern: "^.+$"
description: User's given name for account creation
example: John
last_name:
type: string
minLength: 1
maxLength: 100
pattern: "^.+$"
description: User's family name for account creation
example: Doe
roles:
type: array
minItems: 1
maxItems: 50
items:
type: string
pattern: "^.+$"
maxLength: 100
description: >-
List of role keys to assign in Permit.io (e.g., super_admin, user_admin, auditor, client_user).
Roles must exist in your Permit.io environment. Invalid roles fail silently - check roles_assigned in response.
example: ["user_admin", "auditor"]
AdminUserCreateResponse:
description: Response after creating a user, including Cognito subject ID and sync status
type: object
required:
- cognito_subject_id
- email
- status
- permit_synced
properties:
cognito_subject_id:
type: string
maxLength: 256
pattern: "^.+$"
description: Created user's Cognito unique subject identifier
example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
email:
type: string
format: email
maxLength: 320
description: User email address
example: john.doe@example.com
first_name:
type: string
maxLength: 100
pattern: "^.+$"
description: Created user's given name
example: John
last_name:
type: string
maxLength: 100
pattern: "^.+$"
description: Created user's family name
example: Doe
status:
type: string
maxLength: 50
pattern: "^.+$"
description: Cognito user status
example: FORCE_CHANGE_PASSWORD
enabled:
type: boolean
description: Whether the created user is enabled in Cognito
example: true
permit_synced:
type: boolean
description: >-
Whether user was successfully created in Permit.io authorization system.
If false, user exists in Cognito (can authenticate) but not in Permit.io (has no authorization/permissions).
Manual sync or retry may be required.
example: true
roles_assigned:
type: array
maxItems: 50
items:
type: string
maxLength: 100
pattern: "^.+$"
description: >-
Roles that were successfully assigned in Permit.io. May be a subset of requested roles if some failed.
Compare with requested roles array to detect assignment failures. If null or empty, no roles were assigned
(user has no permissions). Only present if permit_synced is true.
example: ["user_admin", "auditor"]
created_at:
type: string
format: date-time
maxLength: 50
description: Timestamp when the user was created in the system
example: "2025-10-16T14:23:45Z"
AdminUserDetails:
description: Complete user information from Cognito and Permit.io including roles and timestamps
type: object
required:
- cognito_subject_id
- email
- status
- enabled
properties:
cognito_subject_id:
type: string
maxLength: 256
pattern: "^.+$"
description: User's Cognito unique subject identifier from authentication system
example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
email:
type: string
format: email
maxLength: 320
description: Email address of the user account
example: john.doe@example.com
first_name:
type: string
maxLength: 100
pattern: "^.+$"
description: User's given name retrieved from Cognito
example: John
last_name:
type: string
maxLength: 100
pattern: "^.+$"
description: User's family name retrieved from Cognito
example: Doe
status:
type: string
maxLength: 50
pattern: "^.+$"
description: Current Cognito user account status
example: CONFIRMED
enabled:
type: boolean
description: Current enabled status of the user in Cognito
example: true
roles:
type: array
maxItems: 50
items:
type: string
maxLength: 100
pattern: "^.+$"
description: Roles assigned in Permit.io
example: ["admin", "viewer"]
created_at:
type: string
format: date-time
maxLength: 50
description: Timestamp when the user account was created
example: "2025-10-15T09:15:30Z"
updated_at:
type: string
format: date-time
maxLength: 50
description: Timestamp of last update
example: "2025-10-16T14:20:10Z"
AdminUserUpdate:
description: Request body for updating user attributes (first name, last name, and roles)
type: object
properties:
first_name:
type: string
minLength: 1
maxLength: 100
pattern: "^.+$"
description: Updated given name for the user
example: Jonathan
last_name:
type: string
minLength: 1
maxLength: 100
pattern: "^.+$"
description: Updated family name for the user
example: Doe-Smith
roles:
type: array
minItems: 1
maxItems: 50
items:
type: string
pattern: "^.+$"
maxLength: 100
description: >-
Complete list of role keys to assign in Permit.io - REPLACES all existing roles (not additive).
To add a role, include all current roles plus the new one. To remove a role, omit it from the array.
Roles must exist in Permit.io environment. Invalid roles fail silently - check response.roles for final state.
Omit this field entirely to leave roles unchanged.
example: ["user_admin", "auditor"]
AdminUserList:
description: Paginated list of users with total count and pagination metadata
type: object
required:
- users
- total
- page
- page_size
properties:
users:
type: array
maxItems: 100
items:
$ref: '#/components/schemas/AdminUserDetails'
description: List of users for current page
total:
type: integer
format: int32
minimum: 0
maximum: 2147483647
description: Total number of users matching filter
example: 147
page:
type: integer
format: int32
minimum: 1
maximum: 10000
description: Current page number
example: 1
page_size:
type: integer
format: int32
minimum: 1
maximum: 100
description: Number of users per page
example: 20
has_more:
type: boolean
description: Whether more pages are available
example: true
AdminUserActionResponse:
description: Response for user enable/disable actions with success status and timestamp
type: object
required:
- success
- email
- action
properties:
success:
type: boolean
description: Whether the action succeeded
example: true
email:
type: string
format: email
maxLength: 320
description: Email of the user acted upon
example: john.doe@example.com
action:
type: string
enum: [disable, enable]
description: Action performed
example: disable
cognito_subject_id:
type: string
maxLength: 256
pattern: "^.+$"
description: Cognito subject identifier for the action response
example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
timestamp:
type: string
format: date-time
maxLength: 50
description: Timestamp of the action
example: "2025-10-16T14:25:33Z"
AdminUserDeleteResponse:
description: Response for user deletion with status from each system (Cognito and Permit.io)
type: object
required:
- success
- email
- deleted_from
properties:
success:
type: boolean
description: Whether the deletion succeeded
example: true
email:
type: string
format: email
maxLength: 320
description: Email of the deleted user
example: john.doe@example.com
cognito_subject_id:
type: string
maxLength: 256
pattern: "^.+$"
description: Cognito subject identifier of the deleted user
example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
deleted_from:
type: object
required:
- cognito
- permit
properties:
cognito:
type: boolean
description: Whether user was deleted from Cognito
example: true
permit:
type: boolean
description: Whether user was deleted from Permit.io
example: true
description: Deletion status from each system
timestamp:
type: string
format: date-time
maxLength: 50
description: Timestamp of the deletion
example: "2025-10-16T14:27:15Z"