Task #147: Add structured permission-change audit (users, groups, apps)
Mirrors the existing role-permission audit pattern with a unified
`permission_audit` table capturing actor, target, prev/new id sets, and
timestamp written in the same transaction as the change.
Schema & API
- New `permission_audit` table (target_kind, target_id, change_kind,
actor_user_id, previous_ids[], new_ids[], created_at) with index on
(target_kind, target_id, created_at).
- Transactional audit writes in routes/users.ts (POST/DELETE roles,
PATCH groupIds), routes/groups.ts (PATCH + add/remove members for
users/roles/apps), routes/apps.ts (POST/DELETE permissions).
- Cross-entity mirroring: when group membership changes via a group
endpoint, a user.groups row is also written for each affected user
(and vice versa via PATCH /users), so each entity's history is
exhaustive regardless of which editor was used.
- Admin-only GET /users/:id/audit, /groups/:id/audit, /apps/:id/audit
with limit/offset/actorUserId/from/to filters and the same response
shape as role audit.
- OpenAPI types + codegen regenerated.
UI
- Reusable PermissionAuditHistory component in admin.tsx wired into
UserGroupsEditor, GroupDetailEditor (new "history" tab), and the
editing-app dialog. App history correctly resolves permission ids
(NOT roles) via useListPermissions.
- Bilingual i18n keys added under admin.{users,groups,apps}.history*
in en.json + ar.json.
Tests
- New backend tests: user-permission-audit, group-permission-audit,
app-permission-audit (14 cases — transactional capture, GET filters
& pagination, admin-only, 404 on unknown id, plus 2 new mirror
tests covering cross-entity audit visibility). All pass; 35
adjacent role/groups/users/audit-coverage tests still pass.
Notes
- replit.md updated to list `permission_audit` table.
- Restored opengraph.jpg (an unrelated stray binary diff).
- Code-review comments addressed: cross-entity asymmetry fixed via
mirroring; opengraph.jpg restored.
- Follow-ups proposed: timeline UI improvements, cascade audit on
delete/bulk paths, e2e UI test for History sections.
Replit-Task-Id: 4ba8c8de-8dfd-42c7-a3bc-964a1ed3a1a3
This commit is contained in:
@@ -1846,6 +1846,212 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupDeletionConflict"
|
||||
|
||||
/users/{id}/audit:
|
||||
get:
|
||||
operationId: getUserPermissionAudit
|
||||
tags: [users]
|
||||
summary: List recent permission-change audit entries for a user (admin)
|
||||
description: |
|
||||
Returns permission-change audit records for the given user, newest
|
||||
first. Mirrors the role audit endpoint shape and supports the same
|
||||
`limit`/`offset`/`actorUserId`/`from`/`to` filters. Each entry
|
||||
captures the actor (if known), the previous and new id sets, and
|
||||
the change kind (`user.roles` or `user.groups`).
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: limit
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 200
|
||||
default: 10
|
||||
- in: query
|
||||
name: offset
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 0
|
||||
- in: query
|
||||
name: actorUserId
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
- in: query
|
||||
name: from
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- in: query
|
||||
name: to
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
responses:
|
||||
"200":
|
||||
description: A page of permission-change audit entries for the user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PermissionAuditList"
|
||||
"400":
|
||||
description: Invalid filter parameters
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"404":
|
||||
description: User not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/groups/{id}/audit:
|
||||
get:
|
||||
operationId: getGroupPermissionAudit
|
||||
tags: [users]
|
||||
summary: List recent permission-change audit entries for a group (admin)
|
||||
description: |
|
||||
Returns permission-change audit records for the given group, newest
|
||||
first. Captures changes to the group's user, role and app sets via
|
||||
the discriminator field `changeKind`.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: limit
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 200
|
||||
default: 10
|
||||
- in: query
|
||||
name: offset
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 0
|
||||
- in: query
|
||||
name: actorUserId
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
- in: query
|
||||
name: from
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- in: query
|
||||
name: to
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
responses:
|
||||
"200":
|
||||
description: A page of permission-change audit entries for the group
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PermissionAuditList"
|
||||
"400":
|
||||
description: Invalid filter parameters
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"404":
|
||||
description: Group not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/apps/{id}/audit:
|
||||
get:
|
||||
operationId: getAppPermissionAudit
|
||||
tags: [apps]
|
||||
summary: List recent permission-change audit entries for an app (admin)
|
||||
description: |
|
||||
Returns permission-change audit records for the given app, newest
|
||||
first. Captures changes to the set of permissions required to open
|
||||
the app (`changeKind: app.permissions`).
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: limit
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 200
|
||||
default: 10
|
||||
- in: query
|
||||
name: offset
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 0
|
||||
- in: query
|
||||
name: actorUserId
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
- in: query
|
||||
name: from
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- in: query
|
||||
name: to
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
responses:
|
||||
"200":
|
||||
description: A page of permission-change audit entries for the app
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PermissionAuditList"
|
||||
"400":
|
||||
description: Invalid filter parameters
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"404":
|
||||
description: App not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
# Stats
|
||||
/stats/home:
|
||||
get:
|
||||
@@ -3864,6 +4070,82 @@ components:
|
||||
- createdAt
|
||||
- actor
|
||||
|
||||
PermissionAuditList:
|
||||
type: object
|
||||
properties:
|
||||
entries:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/PermissionAuditEntry"
|
||||
totalCount:
|
||||
type: integer
|
||||
limit:
|
||||
type: integer
|
||||
offset:
|
||||
type: integer
|
||||
nextOffset:
|
||||
type: ["integer", "null"]
|
||||
required:
|
||||
- entries
|
||||
- totalCount
|
||||
- limit
|
||||
- offset
|
||||
- nextOffset
|
||||
|
||||
PermissionAuditEntry:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
targetKind:
|
||||
type: string
|
||||
enum: [user, group, app]
|
||||
targetId:
|
||||
type: integer
|
||||
changeKind:
|
||||
type: string
|
||||
enum:
|
||||
- user.roles
|
||||
- user.groups
|
||||
- group.users
|
||||
- group.roles
|
||||
- group.apps
|
||||
- app.permissions
|
||||
previousIds:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
newIds:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
addedIds:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
removedIds:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
actor:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/AuditLogActor"
|
||||
- type: "null"
|
||||
required:
|
||||
- id
|
||||
- targetKind
|
||||
- targetId
|
||||
- changeKind
|
||||
- previousIds
|
||||
- newIds
|
||||
- addedIds
|
||||
- removedIds
|
||||
- createdAt
|
||||
- actor
|
||||
|
||||
AuditLogEntry:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in New Issue
Block a user