Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
bf8ec33
feat: environments api
rohan-chaturvedi Mar 9, 2026
64d6348
fix: use cache-and-network policy on app access pages
rohan-chaturvedi Mar 9, 2026
f71eed0
feat: add apps api
rohan-chaturvedi Mar 9, 2026
ded69a4
feat: rest api for roles, service accounts
rohan-chaturvedi Mar 13, 2026
77e97e5
fix: validate environments when assigning account app access
rohan-chaturvedi Mar 13, 2026
c23a7eb
feat: validate json permissions when creating or updating roles
rohan-chaturvedi Mar 13, 2026
aa10e2b
wip: audit logs
rohan-chaturvedi Mar 14, 2026
6fdcd85
Merge branch 'main' into api--apps-envs-accounts
rohan-chaturvedi Mar 16, 2026
9bb5fc3
Merge branch 'main' into api--apps-envs-accounts
rohan-chaturvedi Mar 16, 2026
5927821
fix: migration graph
rohan-chaturvedi Mar 16, 2026
0184d81
chore: regenerate graphql types
rohan-chaturvedi Mar 16, 2026
b26d78c
fix: enhance captured log event data
rohan-chaturvedi Mar 16, 2026
2c07ca1
feat: rescale and improve audit logs ui
rohan-chaturvedi Mar 16, 2026
6a5fc0d
Merge branch 'main' into api--apps-envs-accounts
rohan-chaturvedi Mar 17, 2026
c9cd7b5
feat: log org invites
rohan-chaturvedi Mar 17, 2026
fc35dcb
fix: access and permissions checks for apps and envs api
rohan-chaturvedi Mar 18, 2026
e0b64ab
fix: remove logs api route
rohan-chaturvedi Mar 18, 2026
b144c2a
feat: add api to manage users and invites, fix service account attrib…
rohan-chaturvedi Mar 18, 2026
9d9f803
fix: use enum values for comparisons
rohan-chaturvedi Mar 18, 2026
e653c1d
fix: apps api tests
rohan-chaturvedi Mar 18, 2026
ccea3e8
feat: filter logs for viewer access level
rohan-chaturvedi Mar 20, 2026
f753b0b
feat: misc improvements to log detail, clarity and correctness
rohan-chaturvedi Mar 20, 2026
3a3e158
feat: several improvements to audit log styling, rendering and detail…
rohan-chaturvedi Mar 20, 2026
53ef095
Merge branch 'main' into api--apps-envs-accounts
rohan-chaturvedi Mar 24, 2026
89a691a
Merge branch 'main' into api--apps-envs-accounts
rohan-chaturvedi Mar 24, 2026
34eefd3
Merge branch 'main' into api--apps-envs-accounts
rohan-chaturvedi Mar 27, 2026
9515bbf
fix: add input validation utilities and harden token parsing
nimish-ks Apr 7, 2026
c110d9c
fix: prevent 500 on cross-org app/environment access
nimish-ks Apr 7, 2026
d67e108
fix: protect Owner from API modification and harden SA member management
nimish-ks Apr 7, 2026
efaaeec
fix: block role escalation via update and accept camelCase permissions
nimish-ks Apr 7, 2026
1729cba
fix: apply input validation across apps, environments, and service ac…
nimish-ks Apr 7, 2026
a3bde33
fix: UnboundLocalError when updating SA role without name
nimish-ks Apr 8, 2026
25f7b1e
Merge remote-tracking branch 'origin/main' into api--apps-envs-accounts
rohan-chaturvedi May 8, 2026
094a9c9
Merge branch 'api--apps-envs-accounts' into api--apps-envs-accounts--…
rohan-chaturvedi May 8, 2026
a0d8334
fix: merge migration to reconcile 0120_invite_sa_inviter and 0122_sso…
rohan-chaturvedi May 9, 2026
e73fa57
fix: harden auth header parsing across all token helpers and clarify …
rohan-chaturvedi May 9, 2026
8de2173
fix: PublicInviteDetailView returns 405 on unsupported HTTP methods
rohan-chaturvedi May 9, 2026
14f0f02
Merge pull request #843 from phasehq/api--apps-envs-accounts--qa-fixes
rohan-chaturvedi May 9, 2026
0b89ca4
Merge branch 'main' into api--apps-envs-accounts
rohan-chaturvedi May 9, 2026
512e7b0
test: use non-Owner roles for member self-update/delete and missing-r…
rohan-chaturvedi May 9, 2026
4735424
Merge branch 'main' into api--apps-envs-accounts
rohan-chaturvedi May 11, 2026
3628b4a
Merge branch 'main' into api--apps-envs-accounts
rohan-chaturvedi May 12, 2026
3450f25
fix: tighten identity_key guard on member/SA access PUTs to reject nu…
rohan-chaturvedi May 12, 2026
e9bf903
fix: strip C0/C1 control characters from text fields (keep tab/LF/CR)
rohan-chaturvedi May 12, 2026
9e0f890
fix: require permissions, app_permissions, and global_access keys whe…
rohan-chaturvedi May 12, 2026
a369a37
fix: capture role name in service account delete audit log
rohan-chaturvedi May 12, 2026
388d09d
feat: emit audit event when a new member joins via invite acceptance
rohan-chaturvedi May 12, 2026
218c9eb
feat: render avatar+name+email+role cards for member and service acco…
rohan-chaturvedi May 12, 2026
c70fdd4
Merge remote-tracking branch 'origin/main' into api--apps-envs-accounts
rohan-chaturvedi May 18, 2026
58d69b9
feat(access): add service_account_can_access_app team-aware helper
rohan-chaturvedi May 18, 2026
43e0136
feat(rest): apps and environments REST endpoints honour team-derived …
rohan-chaturvedi May 18, 2026
552e05c
feat(rest): track EnvironmentKeyGrant on REST access PUTs so team rev…
rohan-chaturvedi May 18, 2026
6857a9c
feat(rest): route SCIM-managed members through deactivate_scim_user o…
rohan-chaturvedi May 18, 2026
a8dadd4
feat(rest): team-scope SA list and detail visibility, accept team_id …
rohan-chaturvedi May 18, 2026
5b5d10f
feat(rest): reject role updates for pre-login members when the role w…
rohan-chaturvedi May 18, 2026
b185ba5
feat(rest): teams CRUD + members + declarative access endpoints (plan…
rohan-chaturvedi May 18, 2026
8d93a2b
feat(models): add team to AuditEvent resource types and last_used_at …
rohan-chaturvedi May 18, 2026
69f059c
feat(auth): bump ServiceAccountToken.last_used_at on every authentica…
rohan-chaturvedi May 18, 2026
a21fdb2
chore(gql): regenerate schema and frontend types for team audit resou…
rohan-chaturvedi May 18, 2026
245f87b
feat(logs): add teams tab and title-case team resource label in audit…
rohan-chaturvedi May 18, 2026
2d4102f
perf(sa-token): short-circuit on last_used_at and backfill it from Se…
rohan-chaturvedi May 18, 2026
aebea47
fix(logs): match skeleton row height to real rows to eliminate layout…
rohan-chaturvedi May 18, 2026
15b92e0
Merge branch 'main' into api--apps-envs-accounts
rohan-chaturvedi May 18, 2026
41345ed
test: provide third side_effect entry and audit path mocks for Update…
rohan-chaturvedi May 18, 2026
1dcc8c8
Merge branch 'main' into api--apps-envs-accounts
nimish-ks May 19, 2026
0076f52
fix(teams-rest): auto-add SA caller as team member on create and appl…
rohan-chaturvedi May 19, 2026
063a8ab
feat(audit): capture token attribution and log team CRUD, membership,…
rohan-chaturvedi May 19, 2026
fb1b4fb
feat(audit-logs-ui): show token via for actor, link actor/token/team …
rohan-chaturvedi May 19, 2026
c736184
security: gate REST member/SA access on caller's own app/env access, …
rohan-chaturvedi May 19, 2026
8ce1fc6
fix(auth): make SA app-auth team-aware so team-owned SAs aren't rejec…
rohan-chaturvedi May 19, 2026
9557dc3
fix(rest-api): return 401 for unauth, list all apps incl. E2EE, JSON-…
rohan-chaturvedi May 20, 2026
6c98408
fix(api): strip internal role meta from responses (promote default-ro…
rohan-chaturvedi May 20, 2026
15226f3
fix(audit-logs): pagination merge via typePolicy, freeze Count from-v…
rohan-chaturvedi May 20, 2026
ba5b42e
feat(rest-api): add POST/DELETE service-account token endpoints with …
rohan-chaturvedi May 20, 2026
aea6fae
fix(rest-api): auto-create unknown secret tags, echo member access on…
rohan-chaturvedi May 20, 2026
146fb11
feat(audit): log SSE enable on apps and SSK enable/disable on service…
rohan-chaturvedi May 20, 2026
ebff748
feat: renamed the sidebar section and changes icon
nimish-ks May 20, 2026
d250fa0
feat: added audit logs log navgiation in commenad pelette
nimish-ks May 20, 2026
63ce58c
feat: rename the page title to audit logs
nimish-ks May 20, 2026
1334e42
fix(audit-logs): advance queryStart/queryEnd state on refresh so subs…
rohan-chaturvedi May 20, 2026
9007197
fix: sidebar icon
rohan-chaturvedi May 20, 2026
7d8e8bc
security(rest-api): scope DELETE /v1/secrets/ by environment to block…
rohan-chaturvedi May 21, 2026
513f1e0
security: wipe EnvironmentKey wrapping material when a service accoun…
rohan-chaturvedi May 21, 2026
721ec92
security(roles): drop global_access from the custom-role API contract…
rohan-chaturvedi May 21, 2026
970f89b
security(auth): scope Secret-Id/Environment header lookups by caller …
rohan-chaturvedi May 21, 2026
8a8e784
fix(routing): disable APPEND_SLASH so unslashed URLs 404 instead of 3…
rohan-chaturvedi May 21, 2026
64ede27
fix(auth): require Bearer scheme on Authorization header instead of a…
rohan-chaturvedi May 21, 2026
c3a27f6
fix(rest-api): close several Django HTML-500 paths in /v1/secrets and…
rohan-chaturvedi May 21, 2026
64b2443
security(members): block non-global callers from modifying access of …
rohan-chaturvedi May 21, 2026
57d1f29
security(sa-tokens): scope POST/DELETE /v1/service-accounts/:id/token…
rohan-chaturvedi May 21, 2026
9f38bf4
security(access): refuse to modify access for members with a global-a…
rohan-chaturvedi May 21, 2026
3f16a2f
audit(cascade): emit per-env D events on app delete and per-SA/per-to…
rohan-chaturvedi May 21, 2026
dccaaf8
fix(sa-tokens): require expires_in (positive int seconds); reject exp…
rohan-chaturvedi May 21, 2026
8ab6824
audit(sa-tokens): populate new_values on C and old_values on D so nam…
rohan-chaturvedi May 21, 2026
b9b854d
Merge branch 'main' into api--apps-envs-accounts
rohan-chaturvedi May 25, 2026
93e0787
feat(sa-tokens): accept expires_at alongside expires_in (IaC round-tr…
rohan-chaturvedi May 25, 2026
08b7247
feat(rest): wrap list-endpoint responses in {data: [...]} for apps/en…
rohan-chaturvedi May 25, 2026
551f08a
feat(rest): inline auto-created children on POST /apps/ (environments…
rohan-chaturvedi May 25, 2026
e3f8c52
security(audit+members): scope sa audit events to visible SAs; block …
rohan-chaturvedi May 25, 2026
cb22c92
feat(rest): split member CRUD from invites — POST /v1/members/ reserv…
rohan-chaturvedi May 25, 2026
8e0bed5
fix(invites): reject 'apps' on POST /v1/members/invites/ — invitee id…
rohan-chaturvedi May 25, 2026
c7652c6
fix(access+audit): attach individual grants on team-shadowed EnvKeys;…
rohan-chaturvedi May 25, 2026
d381e63
Merge branch 'main' into api--apps-envs-accounts
rohan-chaturvedi May 25, 2026
c718a6c
fix(audit-logs): send lowercase resource_type values (matches DB) on …
rohan-chaturvedi May 25, 2026
e41d46b
feat(audit-logs): include service accounts in the Account filter
rohan-chaturvedi May 25, 2026
2bf8354
chore: cleanup comments / doc string
nimish-ks May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,8 @@ yarn-error.log*
next-env.d.ts

# phase
*.phase.json
*.phase.json

# graphql_schema --out artifact — regenerated locally; only the
# frontend/apollo/schema.graphql copy is tracked.
backend/schema.graphql
273 changes: 224 additions & 49 deletions backend/api/auth.py

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions backend/api/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,18 @@ def send_login_email(request, email, full_name, provider):
)


def _get_invite_sender_name(invite):
if invite.invited_by:
return get_org_member_name(invite.invited_by)
if invite.invited_by_service_account:
return invite.invited_by_service_account.name
return invite.organisation.name


def send_invite_email(invite):
organisation = invite.organisation.name

invited_by_name = get_org_member_name(invite.invited_by)
invited_by_name = _get_invite_sender_name(invite)

invite_code = encode_string_to_base64(str(invite.id))

Expand Down Expand Up @@ -131,7 +139,7 @@ def send_user_joined_email(invite, new_member):

owner_name = get_org_member_name(owner)

invited_by_name = get_org_member_name(invite.invited_by)
invited_by_name = _get_invite_sender_name(invite)

if owner_name == invited_by_name:
invited_by_name = "you"
Expand Down
39 changes: 39 additions & 0 deletions backend/api/migrations/0118_auditevent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Generated by Django 4.2.28 on 2026-03-13 11:18

from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import uuid


class Migration(migrations.Migration):

dependencies = [
('api', '0117_alter_environmentsync_service_and_more'),
]

operations = [
migrations.CreateModel(
name='AuditEvent',
fields=[
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('event_type', models.CharField(choices=[('C', 'Create'), ('R', 'Read'), ('U', 'Update'), ('D', 'Delete'), ('A', 'Access')], max_length=1)),
('resource_type', models.CharField(choices=[('app', 'App'), ('env', 'Environment'), ('role', 'Role'), ('sa', 'ServiceAccount'), ('member', 'OrganisationMember'), ('policy', 'NetworkAccessPolicy'), ('pat', 'UserToken'), ('sa_token', 'ServiceAccountToken'), ('svc_token', 'ServiceToken')], max_length=10)),
('resource_id', models.TextField()),
('actor_type', models.CharField(choices=[('user', 'User'), ('sa', 'ServiceAccount')], max_length=10)),
('actor_id', models.TextField()),
('actor_metadata', models.JSONField(default=dict)),
('resource_metadata', models.JSONField(default=dict)),
('old_values', models.JSONField(blank=True, null=True)),
('new_values', models.JSONField(blank=True, null=True)),
('description', models.TextField(blank=True, default='')),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('user_agent', models.TextField(blank=True, default='')),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='audit_events', to='api.organisation')),
],
options={
'indexes': [models.Index(fields=['resource_type', 'resource_id', '-timestamp'], name='audit_resource_history_idx'), models.Index(fields=['organisation', '-timestamp'], name='audit_org_activity_idx'), models.Index(fields=['actor_type', 'actor_id', '-timestamp'], name='audit_actor_activity_idx')],
},
),
]
18 changes: 18 additions & 0 deletions backend/api/migrations/0119_auditevent_invite_resource_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.29 on 2026-03-17 14:46

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0118_auditevent'),
]

operations = [
migrations.AlterField(
model_name='auditevent',
name='resource_type',
field=models.CharField(choices=[('app', 'App'), ('env', 'Environment'), ('role', 'Role'), ('sa', 'ServiceAccount'), ('member', 'OrganisationMember'), ('policy', 'NetworkAccessPolicy'), ('pat', 'UserToken'), ('sa_token', 'ServiceAccountToken'), ('svc_token', 'ServiceToken'), ('invite', 'Invite')], max_length=10),
),
]
24 changes: 24 additions & 0 deletions backend/api/migrations/0120_invite_sa_inviter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.29 on 2026-03-18 08:56

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('api', '0119_auditevent_invite_resource_type'),
]

operations = [
migrations.AlterField(
model_name='organisationmemberinvite',
name='invited_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.organisationmember'),
),
migrations.AddField(
model_name='organisationmemberinvite',
name='invited_by_service_account',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.serviceaccount'),
),
]
14 changes: 14 additions & 0 deletions backend/api/migrations/0123_merge_20260509_1024.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 4.2.29 on 2026-05-09 10:24

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('api', '0120_invite_sa_inviter'),
('api', '0122_sso_audit_fks_set_null'),
]

operations = [
]
14 changes: 14 additions & 0 deletions backend/api/migrations/0127_merge_20260518_0815.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 4.2.30 on 2026-05-18 08:15

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('api', '0123_merge_20260509_1024'),
('api', '0126_populate_team_owner'),
]

operations = [
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.30 on 2026-05-18 10:23

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0127_merge_20260518_0815'),
]

operations = [
migrations.AddField(
model_name='serviceaccounttoken',
name='last_used_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='auditevent',
name='resource_type',
field=models.CharField(choices=[('app', 'App'), ('env', 'Environment'), ('role', 'Role'), ('sa', 'ServiceAccount'), ('member', 'OrganisationMember'), ('policy', 'NetworkAccessPolicy'), ('pat', 'UserToken'), ('sa_token', 'ServiceAccountToken'), ('svc_token', 'ServiceToken'), ('invite', 'Invite'), ('team', 'Team')], max_length=10),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Backfill: EnvironmentKey rows for service accounts that were soft-deleted
before the SA delete cascade also wiped EnvironmentKeys still had live
wrapped_seed / wrapped_salt material. Soft-delete those rows and clear
the wrapping fields so a DB-compromise on backup can't recover the
deleted principal's env access.
"""
from django.db import migrations
from django.utils import timezone


def wipe_orphan_envkeys(apps, schema_editor):
EnvironmentKey = apps.get_model("api", "EnvironmentKey")
orphans = EnvironmentKey.objects.filter(
service_account__deleted_at__isnull=False,
deleted_at__isnull=True,
)
orphans.update(
deleted_at=timezone.now(),
wrapped_seed="",
wrapped_salt="",
identity_key="",
)


def noop_reverse(apps, schema_editor):
# Irreversible — we've zeroed the wrapping material on purpose.
pass


class Migration(migrations.Migration):

dependencies = [
("api", "0128_add_team_audit_resource_and_sa_token_last_used"),
]

operations = [
migrations.RunPython(wipe_orphan_envkeys, noop_reverse),
]
125 changes: 120 additions & 5 deletions backend/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,13 +357,26 @@ class ServiceAccount(models.Model):

def delete(self, *args, **kwargs):
"""
Soft delete the object by setting the 'deleted_at' field.
Soft delete the SA, its tokens, and its EnvironmentKey rows
(wiping wrapping material so an attacker with DB access can't
recover env seeds belonging to a deleted principal).
"""
self.deleted_at = timezone.now()
now = timezone.now()
self.deleted_at = now
self.save()

# Soft-delete related tokens
self.serviceaccounttoken_set.update(deleted_at=timezone.now())
self.serviceaccounttoken_set.filter(deleted_at__isnull=True).update(
deleted_at=now
)

EnvironmentKey.objects.filter(
service_account=self, deleted_at__isnull=True
).update(
deleted_at=now,
wrapped_seed="",
wrapped_salt="",
identity_key="",
)


class ServiceAccountHandler(models.Model):
Expand Down Expand Up @@ -398,7 +411,12 @@ class OrganisationMemberInvite(models.Model):
null=True,
blank=True,
)
invited_by = models.ForeignKey(OrganisationMember, on_delete=models.CASCADE)
invited_by = models.ForeignKey(
OrganisationMember, on_delete=models.SET_NULL, null=True, blank=True
)
invited_by_service_account = models.ForeignKey(
"ServiceAccount", on_delete=models.SET_NULL, null=True, blank=True
)
invitee_email = models.EmailField()
valid = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)
Expand Down Expand Up @@ -638,6 +656,11 @@ class ServiceAccountToken(models.Model):
updated_at = models.DateTimeField(auto_now=True)
deleted_at = models.DateTimeField(blank=True, null=True)
expires_at = models.DateTimeField(null=True)
# Bumped on every successful authentication via the REST/management API.
# The legacy `last_used` resolver fell back to SecretEvent history, which
# only fires for E2EE secret operations and misses management-API usage
# entirely. This direct field makes "Last used" accurate across both.
last_used_at = models.DateTimeField(blank=True, null=True)

def clean(self):
# Ensure only one of created_by or created_by_service_account is set
Expand Down Expand Up @@ -1010,6 +1033,98 @@ class Meta:
user_agent = models.TextField(null=True, blank=True)


class AuditEvent(models.Model):
"""
Generic audit log for organisation-level events (Apps, Environments, Roles,
Service Accounts, Members, Tokens, Network Policies).
SecretEvent remains separate for encrypted-value logging.
"""

CREATE = "C"
READ = "R"
UPDATE = "U"
DELETE = "D"
ACCESS = "A"
EVENT_TYPES = [
(CREATE, "Create"),
(READ, "Read"),
(UPDATE, "Update"),
(DELETE, "Delete"),
(ACCESS, "Access"),
]

APP = "app"
ENVIRONMENT = "env"
ROLE = "role"
SERVICE_ACCOUNT = "sa"
ORG_MEMBER = "member"
NETWORK_POLICY = "policy"
USER_TOKEN = "pat"
SA_TOKEN = "sa_token"
SERVICE_TOKEN = "svc_token"
INVITE = "invite"
TEAM = "team"
RESOURCE_TYPES = [
(APP, "App"),
(ENVIRONMENT, "Environment"),
(ROLE, "Role"),
(SERVICE_ACCOUNT, "ServiceAccount"),
(ORG_MEMBER, "OrganisationMember"),
(NETWORK_POLICY, "NetworkAccessPolicy"),
(USER_TOKEN, "UserToken"),
(SA_TOKEN, "ServiceAccountToken"),
(SERVICE_TOKEN, "ServiceToken"),
(INVITE, "Invite"),
(TEAM, "Team"),
]

class Meta:
indexes = [
models.Index(
fields=["resource_type", "resource_id", "-timestamp"],
name="audit_resource_history_idx",
),
models.Index(
fields=["organisation", "-timestamp"],
name="audit_org_activity_idx",
),
models.Index(
fields=["actor_type", "actor_id", "-timestamp"],
name="audit_actor_activity_idx",
),
]

id = models.TextField(default=uuid4, primary_key=True, editable=False)
organisation = models.ForeignKey(
Organisation, on_delete=models.CASCADE, related_name="audit_events"
)

# What happened
event_type = models.CharField(max_length=1, choices=EVENT_TYPES)
resource_type = models.CharField(max_length=10, choices=RESOURCE_TYPES)
resource_id = models.TextField()

# Who did it (no ForeignKey — survives entity deletion)
actor_type = models.CharField(
max_length=10,
choices=[("user", "User"), ("sa", "ServiceAccount")],
)
actor_id = models.TextField()
actor_metadata = models.JSONField(default=dict)

# What changed
resource_metadata = models.JSONField(default=dict)
old_values = models.JSONField(null=True, blank=True)
new_values = models.JSONField(null=True, blank=True)
description = models.TextField(blank=True, default="")

# Request metadata
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(blank=True, default="")

timestamp = models.DateTimeField(default=timezone.now)


class Identity(models.Model):
"""
Third-party identity configuration.
Expand Down
Loading
Loading