diff --git a/api/audit_log.rst b/api/audit_log.rst new file mode 100644 index 00000000..a4736407 --- /dev/null +++ b/api/audit_log.rst @@ -0,0 +1,375 @@ +Audit Log +========= + +The audit log records security-relevant changes to your Zammad system: +who changed what, and when. The audit log is read-only. + +List +---- + +Required permission: ``admin.audit_log`` + +``GET``-Request sent: ``/api/v1/audit_logs`` + +The endpoint supports pagination. The default page size is ``500``. +Entries are returned ordered by ``id`` (ascending). Pass ``?sort_by=id`` +and ``?order_by=DESC`` to return the newest entries first. + +Response: + +.. code-block:: json + :force: + + # HTTP-Code 200 Ok + + [ + { + "id": 1, + "user_id": 3, + "user_fullname": "Christopher Miller", + "action_type": "create", + "auditable_type": "ChecklistTemplate", + "auditable_id": 1, + "auditable_name": "Onboarding", + "value_from": {}, + "value_to": { + "name": "Onboarding", + "active": true, + "sorted_item_names": [ + "Buy chair", + "Coffee briefing", + "Create accounts" + ] + }, + "source_ip": "172.19.0.1", + "preferences": {}, + "created_at": "2026-07-23T09:14:51.650Z", + "updated_at": "2026-07-23T09:14:51.650Z" + }, + { + "id": 2, + "user_id": 3, + "user_fullname": "Christopher Miller", + "action_type": "update", + "auditable_type": "Setting", + "auditable_id": 280, + "auditable_name": "ai_provider", + "value_from": { + "area": "AI::Provider", + "name": "ai_provider", + "title": "AI provider", + "options": {}, + "frontend": true, + "description": "Defines if the AI provider is configured.", + "state_current": { + "value": true + }, + "state_initial": { + "value": false + } + }, + "value_to": { + "area": "AI::Provider", + "name": "ai_provider", + "title": "AI provider", + "options": {}, + "frontend": true, + "description": "Defines if the AI provider is configured.", + "state_current": { + "value": false + }, + "state_initial": { + "value": false + } + }, + "source_ip": "172.19.0.1", + "preferences": { + "changed_attributes": [ + "state_current" + ] + }, + "created_at": "2026-07-23T09:14:59.501Z", + "updated_at": "2026-07-23T09:14:59.501Z" + }, + { + "id": 3, + "user_id": 3, + "user_fullname": "Christopher Miller", + "action_type": "update", + "auditable_type": "Role", + "auditable_id": 2, + "auditable_name": "Agent", + "value_from": {}, + "value_to": { + "permissions": [ + "admin.text_module" + ], + "group_permissions": { + "2nd Level": [ + "full" + ], + "Service Desk": [ + "full" + ] + } + }, + "source_ip": "172.19.0.1", + "preferences": {}, + "created_at": "2026-07-23T09:16:30.188Z", + "updated_at": "2026-07-23T09:16:30.188Z" + }, + { + "id": 4, + "user_id": 3, + "user_fullname": "Christopher Miller", + "action_type": "update", + "auditable_type": "Setting", + "auditable_id": 265, + "auditable_name": "pgp_integration", + "value_from": { + "area": "Integration::Switch", + "name": "pgp_integration", + "title": "PGP integration", + "options": { + "form": [ + { + "tag": "boolean", + "name": "pgp_integration", + "null": true, + "display": "", + "options": { + "true": "yes", + "false": "no" + } + } + ] + }, + "frontend": true, + "description": "Defines if PGP encryption is enabled or not.", + "state_current": { + "value": false + }, + "state_initial": { + "value": false + } + }, + "value_to": { + "area": "Integration::Switch", + "name": "pgp_integration", + "title": "PGP integration", + "options": { + "form": [ + { + "tag": "boolean", + "name": "pgp_integration", + "null": true, + "display": "", + "options": { + "true": "yes", + "false": "no" + } + } + ] + }, + "frontend": true, + "description": "Defines if PGP encryption is enabled or not.", + "state_current": { + "value": true + }, + "state_initial": { + "value": false + } + }, + "source_ip": "172.19.0.1", + "preferences": { + "changed_attributes": [ + "state_current" + ] + }, + "created_at": "2026-07-23T09:17:20.469Z", + "updated_at": "2026-07-23T09:17:20.469Z" + } + ] + + +Show +---- + +Required permission: ``admin.audit_log`` + +``GET``-Request sent: ``/api/v1/audit_logs/{id}`` + +Response: + +.. code-block:: json + :force: + + # HTTP-Code 200 Ok + + { + "id": 3, + "user_id": 3, + "user_fullname": "Christopher Miller", + "action_type": "update", + "auditable_type": "Role", + "auditable_id": 2, + "auditable_name": "Agent", + "value_from": {}, + "value_to": { + "permissions": [ + "admin.text_module" + ], + "group_permissions": { + "2nd Level": [ + "full" + ], + "Service Desk": [ + "full" + ] + } + }, + "source_ip": "172.19.0.1", + "preferences": {}, + "created_at": "2026-07-23T09:16:30.188Z", + "updated_at": "2026-07-23T09:16:30.188Z" + } + +Search +------ + +Required permission: ``admin.audit_log`` + +The search endpoint accepts the Zammad search-backend ``query`` +syntax. The simplest case is a literal substring on a single +indexed field such as ``auditable_name``, ``auditable_type`` or +``user_fullname``: + +``GET``-Request sent: ``/api/v1/audit_logs/search?query={search-string}`` + +Response: + +.. code-block:: json + :force: + + # HTTP-Code 200 Ok + + [ + { + "id": 3, + "user_id": 3, + "user_fullname": "Christopher Miller", + "action_type": "update", + "auditable_type": "Role", + "auditable_id": 2, + "auditable_name": "Agent", + "value_from": {}, + "value_to": { + "permissions": [ + "admin.text_module" + ], + "group_permissions": { + "2nd Level": [ + "full" + ], + "Service Desk": [ + "full" + ] + } + }, + "source_ip": "172.19.0.1", + "preferences": {}, + "created_at": "2026-07-23T09:16:30.188Z", + "updated_at": "2026-07-23T09:16:30.188Z" + } + ] + +To filter on a specific attribute rather than substring-match the +whole record, prefix the attribute name. You can even use the logical ``AND`` +connector to narrow down the results: + +``GET``-Request sent: +``/api/v1/audit_logs/search?query=auditable_type:Setting AND user_id:3`` + +.. note:: + + Search matches are case-sensitive and search only the indexed + attribute fields (``auditable_name``, ``auditable_type``, + ``user_fullname`` and so on). The ``value_from`` and ``value_to`` + payloads are not searchable. + +.. note:: + + By default the response is a bare JSON array of matching entries. + Pass ``with_total_count=true`` on the URL (or ``with_total_count: + true`` in the body of a ``POST`` request) to wrap the response in + an object that also contains the ``total_count``. Send a ``POST`` + request when the query is too long or complex for a URL. + +Field Reference +--------------- + +``id`` + Integer primary key of the audit log entry. + +``user_id`` + ID of the user that triggered the change. ``null`` when the entry + was written by a background job without a current user. + +``user_fullname`` + Full name of the user at the time the entry was written. Stored + separately so it remains readable after the user account is + removed. + +``action_type`` + Type of the recorded change. One of: + + - ``create`` - a record was added. + - ``update`` - an existing record was modified. + - ``destroy`` - a record was removed. + - ``switch_to`` - a user took over another user's session via + ``View from user's perspective`` + - ``switch_back_to`` - the original session was resumed. + +``auditable_id`` + ID of the record that was changed. + +``auditable_type`` + Class name of the record that was changed (e.g. ``Role``, + ``User``, ``Setting``, ``KnowledgeBase``, ``ChecklistTemplate``). + +``auditable_name`` + Display name of the changed record at the time the entry was + written. Stored separately so it remains readable after the record + itself is gone. + +``value_from`` + Object (JSON) holding the previous state of the audited + attributes. Empty (``{}``) on ``create`` entries. + +``value_to`` + Object (JSON) holding the new state of the audited attributes. + Empty (``{}``) on ``destroy`` entries. + +``source_ip`` + IP address that issued the underlying request. ``Rails console`` + or ``Rails runner`` is stored when the entry was written from a + maintenance script. + +``preferences`` + Object (JSON) holding additional per-entry metadata. For + ``update`` entries this contains a ``changed_attributes`` array + listing the attributes that actually changed. + +``created_at`` + Timestamp at which the entry was written. Audit log entries are + append-only. + +``updated_at`` + Same as ``created_at`` for audit log entries. Audit log entries + are append-only. + +Lifecycle +--------- + +A scheduled task removes entries older than ``12 months`` every day. +Use the ``AuditLog.cleanup`` in the Rails console to trigger a cleanup manually. diff --git a/index.rst b/index.rst index 6a648228..399a9f8a 100644 --- a/index.rst +++ b/index.rst @@ -71,6 +71,7 @@ Zammad System Documentation :caption: REST API /api/intro + /api/audit_log /api/calendar /api/checklist /api/generic-cti/index