-
Notifications
You must be signed in to change notification settings - Fork 1
feat: implement audit logging system #122 #134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
70f6369
feat: implement audit logging system #122
d2fb19d
feat: add migration
f3e6cb2
refactor: update audit logging system and remove unused state management
580aa39
feat: enhance audit logging system
5bf2579
Minor improvements and fixes.
Fedir-Yatsenko d871f8f
Merge branch 'development' into 122-audit-log
Fedir-Yatsenko 2c293a8
Fix Enum values
Fedir-Yatsenko c0549b0
feat: make audit log trace_id non-nullable
Fedir-Yatsenko 68f0dc7
Merge branch 'development' into 122-audit-log
kryachkow 105fde7
Fix type hint
Fedir-Yatsenko 3e314d2
TEMP:LOG_TOKEN
4c82ed5
refactor: change of performed_by claims extraction and default values
0386b48
refactor: reforamt
2636958
Merge branch 'development' into 122-audit-log
kryachkow ff34372
feat: add date range filtering to audit logs
0595455
refactor: update claim extraction methods and default values for OIDC…
a1ce938
refactor: simplify OIDC audit claims types and extraction method
32ec2c5
Merge branch 'development' into 122-audit-log
kryachkow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
statgpt/admin/alembic/versions/2026_02_11_1200-3b8a6a40f1cd_add_audit_logs_table.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| """Add immutable audit logs table | ||
|
|
||
| Revision ID: 3b8a6a40f1cd | ||
| Revises: c7f068b2d47d | ||
| Create Date: 2026-02-11 12:00:00.000000 | ||
|
|
||
| """ | ||
|
|
||
| from collections.abc import Sequence | ||
|
|
||
| import sqlalchemy as sa | ||
| from alembic import op | ||
| from sqlalchemy.dialects import postgresql | ||
|
|
||
| # revision identifiers, used by Alembic. | ||
| revision: str = '3b8a6a40f1cd' | ||
| down_revision: str | None = 'c7f068b2d47d' | ||
| branch_labels: str | Sequence[str] | None = None | ||
| depends_on: str | Sequence[str] | None = None | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| audit_entity_type = postgresql.ENUM( | ||
| 'channel', | ||
| 'dataset', | ||
| 'data_source', | ||
| 'import_job', | ||
| name='auditentitytype', | ||
| create_type=False, | ||
| ) | ||
| audit_action_type = postgresql.ENUM( | ||
| 'create', | ||
| 'update', | ||
| 'delete', | ||
| name='auditactiontype', | ||
| create_type=False, | ||
| ) | ||
| audit_entity_type.create(op.get_bind(), checkfirst=True) | ||
| audit_action_type.create(op.get_bind(), checkfirst=True) | ||
|
|
||
| op.create_table( | ||
| 'audit_logs', | ||
| sa.Column('id', sa.Integer(), nullable=False), | ||
| sa.Column('entity_type', audit_entity_type, nullable=False), | ||
| sa.Column('action_type', audit_action_type, nullable=False), | ||
| sa.Column('item_id', sa.Integer(), nullable=False), | ||
| sa.Column('entity_id', sa.String(), nullable=False), | ||
| sa.Column('entity_name', sa.String(), nullable=False), | ||
| sa.Column('performed_by', sa.String(), nullable=False), | ||
| sa.Column('performed_by_name', sa.String(), nullable=False), | ||
| sa.Column('state_after', postgresql.JSONB(astext_type=sa.Text()), nullable=True), | ||
| sa.Column('trace_id', sa.String(), nullable=False), | ||
| sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()')), | ||
| sa.PrimaryKeyConstraint('id'), | ||
| ) | ||
| op.create_index('ix_audit_logs_created_at', 'audit_logs', ['created_at'], unique=False) | ||
| op.create_index( | ||
| 'ix_audit_logs_entity_type_entity_id', | ||
| 'audit_logs', | ||
| ['entity_type', 'entity_id'], | ||
| unique=False, | ||
| ) | ||
| op.create_index( | ||
| 'ix_audit_logs_entity_type_item_id', | ||
| 'audit_logs', | ||
| ['entity_type', 'item_id'], | ||
| unique=False, | ||
| ) | ||
| op.execute( | ||
| """ | ||
| CREATE OR REPLACE FUNCTION prevent_audit_log_mutation() | ||
| RETURNS trigger AS $$ | ||
| BEGIN | ||
| RAISE EXCEPTION 'audit_logs rows are immutable'; | ||
| END; | ||
| $$ LANGUAGE plpgsql; | ||
| """ | ||
| ) | ||
| op.execute( | ||
| """ | ||
| CREATE TRIGGER trg_prevent_audit_log_mutation | ||
| BEFORE UPDATE OR DELETE ON audit_logs | ||
| FOR EACH ROW | ||
| EXECUTE FUNCTION prevent_audit_log_mutation(); | ||
| """ | ||
| ) | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| op.execute("DROP TRIGGER IF EXISTS trg_prevent_audit_log_mutation ON audit_logs") | ||
| op.execute("DROP FUNCTION IF EXISTS prevent_audit_log_mutation()") | ||
| op.drop_index('ix_audit_logs_entity_type_item_id', table_name='audit_logs') | ||
| op.drop_index('ix_audit_logs_entity_type_entity_id', table_name='audit_logs') | ||
| op.drop_index('ix_audit_logs_created_at', table_name='audit_logs') | ||
| op.drop_table('audit_logs') | ||
| postgresql.ENUM(name='auditactiontype').drop(op.get_bind(), checkfirst=True) | ||
| postgresql.ENUM(name='auditentitytype').drop(op.get_bind(), checkfirst=True) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| from .context import get_audit_context, set_audit_context | ||
| from .decorators import audit_action |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| from contextvars import ContextVar | ||
| from dataclasses import dataclass, field | ||
|
|
||
| from opentelemetry import trace | ||
|
|
||
|
|
||
| def _get_trace_id() -> str: | ||
| span_context = trace.get_current_span().get_span_context() | ||
| if not span_context.is_valid: | ||
| raise RuntimeError("No valid span context available for trace ID") | ||
| return format(span_context.trace_id, "032x") | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class AuditContext: | ||
| performed_by: str | ||
| performed_by_name: str | ||
| trace_id: str = field(default_factory=_get_trace_id) | ||
|
|
||
|
|
||
| _audit_context_var: ContextVar[AuditContext] = ContextVar("admin_audit_context") | ||
|
|
||
|
|
||
| def set_audit_context(*, performed_by: str, performed_by_name: str) -> None: | ||
| _audit_context_var.set( | ||
| AuditContext( | ||
| performed_by=performed_by, | ||
| performed_by_name=performed_by_name, | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def update_audit_context(audit_context: AuditContext) -> None: | ||
| _audit_context_var.set(audit_context) | ||
|
|
||
|
|
||
| def get_audit_context() -> AuditContext: | ||
| if res := _audit_context_var.get(None): | ||
| return res | ||
| raise RuntimeError("Audit context not set") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import logging | ||
| from collections.abc import Awaitable, Callable | ||
| from functools import wraps | ||
|
|
||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| import statgpt.common.models as models | ||
| from statgpt.admin.audit.context import get_audit_context | ||
| from statgpt.common.schemas.auditable import Auditable | ||
| from statgpt.common.schemas.enums import AuditActionType, AuditEntityType | ||
|
|
||
| _log = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| async def _persist_audit_log( | ||
| *, | ||
| session: AsyncSession, | ||
| entity_type: AuditEntityType, | ||
| action_type: AuditActionType, | ||
| data: Auditable, | ||
| ) -> None: | ||
| context = get_audit_context() | ||
| state_after = None if action_type is AuditActionType.DELETE else data.get_state_after() | ||
|
|
||
| item = models.AuditLog( | ||
| entity_type=entity_type, | ||
| action_type=action_type, | ||
| item_id=data.get_item_id(), | ||
| entity_id=data.get_entity_id(), | ||
| entity_name=data.get_entity_name(), | ||
| performed_by=context.performed_by, | ||
| performed_by_name=context.performed_by_name, | ||
| state_after=state_after, | ||
| trace_id=context.trace_id, | ||
| ) | ||
| session.add(item) | ||
| await session.commit() | ||
|
|
||
|
|
||
| def audit_action( | ||
| *, | ||
| entity_type: AuditEntityType, | ||
| action_type: AuditActionType, | ||
| ): | ||
| def decorator(func: Callable[..., Awaitable[Auditable]]) -> Callable[..., Awaitable[Auditable]]: | ||
| @wraps(func) | ||
| async def wrapped(self, *args, **kwargs) -> Auditable: | ||
| result: Auditable = await func(self, *args, **kwargs) | ||
| try: | ||
| await _persist_audit_log( | ||
| session=self._session, | ||
| entity_type=entity_type, | ||
| action_type=action_type, | ||
| data=result, | ||
| ) | ||
| except Exception: | ||
| _log.exception( | ||
| f"Failed to persist audit log for {entity_type} action={action_type}" | ||
| ) | ||
| # TODO: Probably we should also roll back the session here | ||
|
|
||
| return result | ||
|
|
||
| return wrapped | ||
|
|
||
| return decorator |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.