From 111ff9adaebd1d0d5d621503cd3ea6673270ecbb Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:24:20 +0200 Subject: [PATCH] docs(api): document audit events and fix the private key update path - Add the OpenAPI attribute for GET /audit-events - Document PATCH /security/keys/{uuid} with its uuid parameter instead of /security/keys --- .../Controllers/Api/AuditEventsController.php | 71 ++++ .../Controllers/Api/SecurityController.php | 9 +- openapi.json | 318 ++++++++++++++---- openapi.yaml | 173 +++++++--- ...itEventsAndPrivateKeyUpdateOpenApiTest.php | 24 ++ 5 files changed, 490 insertions(+), 105 deletions(-) create mode 100644 tests/Unit/AuditEventsAndPrivateKeyUpdateOpenApiTest.php diff --git a/app/Http/Controllers/Api/AuditEventsController.php b/app/Http/Controllers/Api/AuditEventsController.php index 19e916dc10..fdb63c3ead 100644 --- a/app/Http/Controllers/Api/AuditEventsController.php +++ b/app/Http/Controllers/Api/AuditEventsController.php @@ -8,9 +8,80 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; +use OpenApi\Attributes as OA; class AuditEventsController extends Controller { + #[OA\Get( + summary: 'List', + description: 'List audit events of the team. Only team admins and owners can view audit events. Actor e-mail, token, metadata, changes, IP address, and user agent are returned only for tokens with the read:sensitive or root permission.', + path: '/audit-events', + operationId: 'list-audit-events', + security: [ + ['bearerAuth' => []], + ], + tags: ['Audit Events'], + parameters: [ + new OA\Parameter(name: 'per_page', in: 'query', description: 'Items per page.', required: false, schema: new OA\Schema(type: 'integer', default: 25, minimum: 1, maximum: 100)), + new OA\Parameter(name: 'page', in: 'query', description: 'Page number.', required: false, schema: new OA\Schema(type: 'integer', default: 1, minimum: 1)), + new OA\Parameter(name: 'search', in: 'query', description: 'Search text.', required: false, schema: new OA\Schema(type: 'string', maxLength: 255)), + new OA\Parameter(name: 'action', in: 'query', description: 'Filter by action. Use all for every action.', required: false, schema: new OA\Schema(type: 'string', maxLength: 255)), + new OA\Parameter(name: 'source', in: 'query', description: 'Filter by source.', required: false, schema: new OA\Schema(type: 'string', enum: ['all', 'ui', 'api', 'mcp', 'webhook', 'system', 'scheduler'])), + ], + responses: [ + new OA\Response( + response: 200, + description: 'A page of audit events, newest first.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'current_page' => ['type' => 'integer'], + 'data' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'id' => ['type' => 'integer'], + 'team_id' => ['type' => 'integer', 'nullable' => true], + 'event' => ['type' => 'string'], + 'source' => ['type' => 'string'], + 'action' => ['type' => 'string'], + 'level' => ['type' => 'string'], + 'actor_type' => ['type' => 'string', 'nullable' => true], + 'actor_id' => ['type' => 'integer', 'nullable' => true], + 'actor_name' => ['type' => 'string', 'nullable' => true], + 'resource_type' => ['type' => 'string', 'nullable' => true], + 'resource_uuid' => ['type' => 'string', 'nullable' => true], + 'resource_name' => ['type' => 'string', 'nullable' => true], + 'description' => ['type' => 'string', 'nullable' => true], + 'created_at' => ['type' => 'string', 'format' => 'date-time'], + ], + ], + ], + 'per_page' => ['type' => 'integer'], + 'last_page' => ['type' => 'integer'], + 'total' => ['type' => 'integer'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 403, + description: 'Only team admins and owners can view audit logs.', + ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), + ] + )] public function index(Request $request): JsonResponse { $teamId = getTeamIdFromToken(); diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php index 036a6e9328..f5b0b0366b 100644 --- a/app/Http/Controllers/Api/SecurityController.php +++ b/app/Http/Controllers/Api/SecurityController.php @@ -253,12 +253,15 @@ class SecurityController extends Controller #[OA\Patch( summary: 'Update', description: 'Update a private key.', - path: '/security/keys', + path: '/security/keys/{uuid}', operationId: 'update-private-key', security: [ ['bearerAuth' => []], ], tags: ['Private Keys'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key UUID', schema: new OA\Schema(type: 'string')), + ], requestBody: new OA\RequestBody( required: true, content: [ @@ -300,6 +303,10 @@ class SecurityController extends Controller response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 404, + description: 'Private Key not found.', + ), new OA\Response( response: 422, ref: '#/components/responses/422', diff --git a/openapi.json b/openapi.json index a500a6d03f..f3a237b856 100644 --- a/openapi.json +++ b/openapi.json @@ -5732,6 +5732,178 @@ ] } }, + "\/audit-events": { + "get": { + "tags": [ + "Audit Events" + ], + "summary": "List", + "description": "List audit events of the team. Only team admins and owners can view audit events. Actor e-mail, token, metadata, changes, IP address, and user agent are returned only for tokens with the read:sensitive or root permission.", + "operationId": "list-audit-events", + "parameters": [ + { + "name": "per_page", + "in": "query", + "description": "Items per page.", + "required": false, + "schema": { + "type": "integer", + "default": 25, + "maximum": 100, + "minimum": 1 + } + }, + { + "name": "page", + "in": "query", + "description": "Page number.", + "required": false, + "schema": { + "type": "integer", + "default": 1, + "minimum": 1 + } + }, + { + "name": "search", + "in": "query", + "description": "Search text.", + "required": false, + "schema": { + "type": "string", + "maxLength": 255 + } + }, + { + "name": "action", + "in": "query", + "description": "Filter by action. Use all for every action.", + "required": false, + "schema": { + "type": "string", + "maxLength": 255 + } + }, + { + "name": "source", + "in": "query", + "description": "Filter by source.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "all", + "ui", + "api", + "mcp", + "webhook", + "system", + "scheduler" + ] + } + } + ], + "responses": { + "200": { + "description": "A page of audit events, newest first.", + "content": { + "application\/json": { + "schema": { + "properties": { + "current_page": { + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "team_id": { + "type": "integer", + "nullable": true + }, + "event": { + "type": "string" + }, + "source": { + "type": "string" + }, + "action": { + "type": "string" + }, + "level": { + "type": "string" + }, + "actor_type": { + "type": "string", + "nullable": true + }, + "actor_id": { + "type": "integer", + "nullable": true + }, + "actor_name": { + "type": "string", + "nullable": true + }, + "resource_type": { + "type": "string", + "nullable": true + }, + "resource_uuid": { + "type": "string", + "nullable": true + }, + "resource_name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + } + }, + "per_page": { + "type": "integer" + }, + "last_page": { + "type": "integer" + }, + "total": { + "type": "integer" + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "403": { + "description": "Only team admins and owners can view audit logs." + }, + "422": { + "$ref": "#\/components\/responses\/422" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "\/cloud-init-scripts": { "get": { "tags": [ @@ -15231,70 +15403,6 @@ "bearerAuth": [] } ] - }, - "patch": { - "tags": [ - "Private Keys" - ], - "summary": "Update", - "description": "Update a private key.", - "operationId": "update-private-key", - "requestBody": { - "required": true, - "content": { - "application\/json": { - "schema": { - "required": [ - "private_key" - ], - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "private_key": { - "type": "string" - } - }, - "type": "object", - "additionalProperties": false - } - } - } - }, - "responses": { - "201": { - "description": "The updated private key's UUID.", - "content": { - "application\/json": { - "schema": { - "properties": { - "uuid": { - "type": "string" - } - }, - "type": "object" - } - } - } - }, - "401": { - "$ref": "#\/components\/responses\/401" - }, - "400": { - "$ref": "#\/components\/responses\/400" - }, - "422": { - "$ref": "#\/components\/responses\/422" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] } }, "\/security\/keys\/{uuid}": { @@ -15409,6 +15517,84 @@ "bearerAuth": [] } ] + }, + "patch": { + "tags": [ + "Private Keys" + ], + "summary": "Update", + "description": "Update a private key.", + "operationId": "update-private-key", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Private Key UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "private_key" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "private_key": { + "type": "string" + } + }, + "type": "object", + "additionalProperties": false + } + } + } + }, + "responses": { + "201": { + "description": "The updated private key's UUID.", + "content": { + "application\/json": { + "schema": { + "properties": { + "uuid": { + "type": "string" + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "description": "Private Key not found." + }, + "422": { + "$ref": "#\/components\/responses\/422" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] } }, "\/servers\/{uuid}\/cloudflare-tunnel": { @@ -24606,6 +24792,10 @@ "name": "Secret Managers", "description": "Secret Managers" }, + { + "name": "Audit Events", + "description": "Audit Events" + }, { "name": "Cloud-init Scripts", "description": "Cloud-init Scripts" diff --git a/openapi.yaml b/openapi.yaml index 16cbdc4a30..405551392b 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3707,6 +3707,86 @@ paths: security: - bearerAuth: [] + /audit-events: + get: + tags: + - 'Audit Events' + summary: List + description: 'List audit events of the team. Only team admins and owners can view audit events. Actor e-mail, token, metadata, changes, IP address, and user agent are returned only for tokens with the read:sensitive or root permission.' + operationId: list-audit-events + parameters: + - + name: per_page + in: query + description: 'Items per page.' + required: false + schema: + type: integer + default: 25 + maximum: 100 + minimum: 1 + - + name: page + in: query + description: 'Page number.' + required: false + schema: + type: integer + default: 1 + minimum: 1 + - + name: search + in: query + description: 'Search text.' + required: false + schema: + type: string + maxLength: 255 + - + name: action + in: query + description: 'Filter by action. Use all for every action.' + required: false + schema: + type: string + maxLength: 255 + - + name: source + in: query + description: 'Filter by source.' + required: false + schema: + type: string + enum: + - all + - ui + - api + - mcp + - webhook + - system + - scheduler + responses: + '200': + description: 'A page of audit events, newest first.' + content: + application/json: + schema: + properties: + current_page: { type: integer } + data: { type: array, items: { type: object, properties: { id: { type: integer }, team_id: { type: integer, nullable: true }, event: { type: string }, source: { type: string }, action: { type: string }, level: { type: string }, actor_type: { type: string, nullable: true }, actor_id: { type: integer, nullable: true }, actor_name: { type: string, nullable: true }, resource_type: { type: string, nullable: true }, resource_uuid: { type: string, nullable: true }, resource_name: { type: string, nullable: true }, description: { type: string, nullable: true }, created_at: { type: string, format: date-time } } } } + per_page: { type: integer } + last_page: { type: integer } + total: { type: integer } + type: object + '401': + $ref: '#/components/responses/401' + '403': + description: 'Only team admins and owners can view audit logs.' + '422': + $ref: '#/components/responses/422' + security: + - + bearerAuth: [] /cloud-init-scripts: get: tags: @@ -9763,46 +9843,6 @@ paths: security: - bearerAuth: [] - patch: - tags: - - 'Private Keys' - summary: Update - description: 'Update a private key.' - operationId: update-private-key - requestBody: - required: true - content: - application/json: - schema: - required: - - private_key - properties: - name: - type: string - description: - type: string - private_key: - type: string - type: object - additionalProperties: false - responses: - '201': - description: "The updated private key's UUID." - content: - application/json: - schema: - properties: - uuid: { type: string } - type: object - '401': - $ref: '#/components/responses/401' - '400': - $ref: '#/components/responses/400' - '422': - $ref: '#/components/responses/422' - security: - - - bearerAuth: [] '/security/keys/{uuid}': get: tags: @@ -9874,6 +9914,56 @@ paths: security: - bearerAuth: [] + patch: + tags: + - 'Private Keys' + summary: Update + description: 'Update a private key.' + operationId: update-private-key + parameters: + - + name: uuid + in: path + description: 'Private Key UUID' + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + required: + - private_key + properties: + name: + type: string + description: + type: string + private_key: + type: string + type: object + additionalProperties: false + responses: + '201': + description: "The updated private key's UUID." + content: + application/json: + schema: + properties: + uuid: { type: string } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + description: 'Private Key not found.' + '422': + $ref: '#/components/responses/422' + security: + - + bearerAuth: [] '/servers/{uuid}/cloudflare-tunnel': get: tags: @@ -15779,6 +15869,9 @@ tags: - name: 'Secret Managers' description: 'Secret Managers' + - + name: 'Audit Events' + description: 'Audit Events' - name: 'Cloud-init Scripts' description: 'Cloud-init Scripts' diff --git a/tests/Unit/AuditEventsAndPrivateKeyUpdateOpenApiTest.php b/tests/Unit/AuditEventsAndPrivateKeyUpdateOpenApiTest.php new file mode 100644 index 0000000000..4af926f3d6 --- /dev/null +++ b/tests/Unit/AuditEventsAndPrivateKeyUpdateOpenApiTest.php @@ -0,0 +1,24 @@ +not->toBeNull() + ->and($operation['operationId'])->toBe('list-audit-events') + ->and($operation['security'])->toBe([['bearerAuth' => []]]) + ->and(collect($operation['parameters'])->pluck('name')->all())->toBe(['per_page', 'page', 'search', 'action', 'source']) + ->and($operation['responses'])->toHaveKeys(['200', '401', '403', '422']); +}); + +test('documents the private key update on the uuid path', function () { + $paths = auditAndPrivateKeyOpenApiDocument()['paths']; + + expect($paths['/security/keys'])->not->toHaveKey('patch') + ->and($paths['/security/keys/{uuid}']['patch']['operationId'])->toBe('update-private-key') + ->and($paths['/security/keys/{uuid}']['patch']['parameters'][0])->toMatchArray(['name' => 'uuid', 'in' => 'path', 'required' => true]); +});