Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
116 commits
Select commit Hold shift + click to select a range
2e9ddba
feat: add Team, TeamMembership, TeamAppEnvironment, EnvironmentKeyGra…
rohan-chaturvedi Mar 31, 2026
64f7e12
feat: add Teams permission resource and team role override resolution…
rohan-chaturvedi Mar 31, 2026
44aae4c
feat: add GraphQL types, mutations, and queries for teams
rohan-chaturvedi Mar 31, 2026
cbb5689
feat: pass app param to permission checks across existing mutations a…
rohan-chaturvedi Mar 31, 2026
1fd088b
feat: add Pro+ plan gating for teams
rohan-chaturvedi Mar 31, 2026
db6eb38
feat: add GraphQL schema, codegen output, and team operation files
rohan-chaturvedi Mar 31, 2026
7f9bac1
feat: add org-level teams list and detail pages with CRUD dialogs
rohan-chaturvedi Mar 31, 2026
7b4df21
feat: add app-level teams page with add, remove, and manage env dialogs
rohan-chaturvedi Mar 31, 2026
a786263
feat: add Teams tab to org and app access layouts
rohan-chaturvedi Mar 31, 2026
80ad661
feat: show team labels, team alerts on dialogs, and team sections on …
rohan-chaturvedi Mar 31, 2026
082ac9a
feat: add TeamLabel pill component, RoleLabel xs size, and ToggleSwit…
rohan-chaturvedi Mar 31, 2026
f10b609
fix: regenerate GraphQL codegen to fix build type mismatch
rohan-chaturvedi Mar 31, 2026
1907a94
fix: restyle add team to app dialog
rohan-chaturvedi Mar 31, 2026
be55f3e
fix: update app-level teams page layout
rohan-chaturvedi Mar 31, 2026
f1ece39
feat: add profile card for org members and service accounts
rohan-chaturvedi Mar 31, 2026
d61084d
feat: restyle teams list table
rohan-chaturvedi Mar 31, 2026
0aeef55
feat: use profile card
rohan-chaturvedi Mar 31, 2026
9844ed7
feat: add team FK to ServiceAccount model
rohan-chaturvedi Apr 13, 2026
a95bdf9
feat: add team-owned SA backend logic
rohan-chaturvedi Apr 13, 2026
ff461ba
feat: update GraphQL schema and codegen for team-owned SAs
rohan-chaturvedi Apr 13, 2026
8a35df7
feat: add team-owned service account UI
rohan-chaturvedi Apr 13, 2026
9a0d71c
fix: misc permission and access check enforcements
rohan-chaturvedi Apr 13, 2026
e38e53c
feat: misc ui and copy tweaks for team service accounts
rohan-chaturvedi Apr 13, 2026
9302d67
feat: replace toggles with checkboxes, misc other ux updates
rohan-chaturvedi Apr 14, 2026
9f57789
feat: misc fixes to access and permission gating
rohan-chaturvedi Apr 14, 2026
25aade9
feat: add new app-level teams permission class
rohan-chaturvedi Apr 14, 2026
536b114
feat: add hook to infer app-level permission based on access grants
rohan-chaturvedi Apr 14, 2026
df465a4
fix: check app-level teams permission server side for teams mutations
rohan-chaturvedi Apr 14, 2026
44f234d
feat: misc frontend permssion and access gating for app actions
rohan-chaturvedi Apr 14, 2026
1f3c590
fix: copy clarification
rohan-chaturvedi Apr 14, 2026
ebd1fb4
feat: command palette actions for teams
rohan-chaturvedi Apr 14, 2026
8cb6fc7
fix: team mutation gates for team owners
rohan-chaturvedi Apr 14, 2026
9f35a82
feat: add a remove app dialog on team page
rohan-chaturvedi Apr 14, 2026
1005ecb
fix: misc permission fixes
rohan-chaturvedi Apr 14, 2026
3c28e40
feat: checkbox stying tweaks for dark theme
rohan-chaturvedi Apr 14, 2026
c77b7ba
feat: show all apps on manage team access dialog, misc ux updates
rohan-chaturvedi Apr 14, 2026
5efa27b
feat: add team ownership set and transfer flow
rohan-chaturvedi Apr 14, 2026
dfd6407
feat: add SCIM models and migrations
rohan-chaturvedi Apr 3, 2026
3f77631
feat: add SCIM quota check and RBAC permissions
rohan-chaturvedi Apr 3, 2026
0228361
feat: add SCIM v2 REST API with user and group provisioning
rohan-chaturvedi Apr 3, 2026
93ddbe3
feat: mount SCIM URLs and add social account auto-linking adapter
rohan-chaturvedi Apr 3, 2026
8a0ecb0
feat: add SCIM GraphQL types, token management mutations, and event q…
rohan-chaturvedi Apr 3, 2026
a3d1051
feat: guard SCIM-managed member and team deletion
rohan-chaturvedi Apr 3, 2026
d46dc4e
fix: convert Ed25519 identity keys to Curve25519 for key wrapping
rohan-chaturvedi Apr 3, 2026
5fbaac4
feat: add SCIM GraphQL operations and scimManaged/scimEnabled fields …
rohan-chaturvedi Apr 3, 2026
1b2aaba
chore: regenerate GraphQL schema and codegen types for SCIM
rohan-chaturvedi Apr 3, 2026
86a48c5
feat: add SCIM and Pending badges to ProfileCard with 3xs font size
rohan-chaturvedi Apr 3, 2026
375c0ba
feat: add SCIM tab and integrate SCIM badges, guards, and ProfileCard…
rohan-chaturvedi Apr 3, 2026
7757aa0
feat: add SCIM settings page with token management, audit logs, and p…
rohan-chaturvedi Apr 3, 2026
fdae663
feat: add account-init redirect for SCIM-provisioned users without ke…
rohan-chaturvedi Apr 3, 2026
b4843c0
fix: team settings for scim managed teams
rohan-chaturvedi Apr 3, 2026
47cd26d
fix: group description sync
rohan-chaturvedi Apr 4, 2026
1f21a28
feat: misc improvements to provisioning logs
rohan-chaturvedi Apr 4, 2026
723caeb
fix: misc fixes to support okta scim
rohan-chaturvedi Apr 4, 2026
e686a27
feat: misc code organisation and cleanup
rohan-chaturvedi Apr 6, 2026
a480f5f
fix: clear team memberships when an scim user is deactivated
rohan-chaturvedi Apr 7, 2026
7a21c27
fix: discovery base url for users and groups
rohan-chaturvedi Apr 7, 2026
bf3fe2f
feat: add unit tests for scim utils, handlers, auth, lifecycle
rohan-chaturvedi Apr 8, 2026
1b812c3
fix: tests
rohan-chaturvedi Apr 8, 2026
b8c078c
fix: use sqlite for integration tests
rohan-chaturvedi Apr 8, 2026
7f4f6dd
fix: conftest for sqlite
rohan-chaturvedi Apr 8, 2026
0dd1334
feat: use mock-based tests
rohan-chaturvedi Apr 8, 2026
77a844c
fix: bump python-version for backend build to 3.12
rohan-chaturvedi Apr 8, 2026
cc6b644
fix: conftest secrets
rohan-chaturvedi Apr 8, 2026
edf69c0
chore: regen schema and types
rohan-chaturvedi Apr 14, 2026
9533387
chore: clean up migration graph
rohan-chaturvedi Apr 14, 2026
ce59169
fix: user profiles in transfer team ownership dialog
rohan-chaturvedi Apr 14, 2026
8f97a91
feat: team-owned service account backend support
rohan-chaturvedi Apr 15, 2026
529ddda
fix: SA auth error handling, soft-delete filter, and union permission…
rohan-chaturvedi Apr 15, 2026
761491d
feat: effective permissions and server-side token generation for team…
rohan-chaturvedi Apr 15, 2026
6e68b08
fix: refetch team data on SA create and delete
rohan-chaturvedi Apr 15, 2026
216dd49
chore: regen schema and types
rohan-chaturvedi Apr 15, 2026
6a39208
fix: prevent team-owned service accounts for being changed to client-…
rohan-chaturvedi Apr 15, 2026
8c7079f
fix: preserve environment key grants on individual creation and scope…
rohan-chaturvedi Apr 18, 2026
a59850c
fix: filter soft-deleted keys in user_can_access_environment
rohan-chaturvedi Apr 18, 2026
82e9689
fix: scope UpdateServiceAccountHandlers deletes and require SA update…
rohan-chaturvedi Apr 18, 2026
9320103
fix: skip service accounts without a self handler during handler re-wrap
rohan-chaturvedi Apr 18, 2026
fd7f23f
fix: await team refetch on SA create and gate delete by SA permission
rohan-chaturvedi Apr 18, 2026
186d6a7
feat: team owners retain org role for team-accessed apps
rohan-chaturvedi Apr 18, 2026
8011483
fix: exclude team-owned service accounts from app add dialog
rohan-chaturvedi Apr 18, 2026
bf09ef6
feat: surface existing team-based app access in add member and SA dia…
rohan-chaturvedi Apr 18, 2026
9afc796
fix: clean up team-owned service accounts on SCIM group deletion
rohan-chaturvedi Apr 18, 2026
90e7c47
fix: recover from concurrent environment key creation races
rohan-chaturvedi Apr 18, 2026
9a232b0
fix: wrap multi-step team mutations in atomic transactions
rohan-chaturvedi Apr 18, 2026
b584bb6
refactor: remove service account ownership transfer due to crypto and…
rohan-chaturvedi Apr 18, 2026
7e56eeb
fix: avoid leaking exception messages in SCIM error responses
rohan-chaturvedi Apr 18, 2026
3f42f73
fix: cap SCIM filter length to prevent polynomial ReDoS
rohan-chaturvedi Apr 18, 2026
b1c99b1
test: mock ServiceAccount in SCIM group delete tests
rohan-chaturvedi Apr 18, 2026
1c5d911
feat: paywall CreateTeamDialog behind Pro plan on free tier
rohan-chaturvedi Apr 18, 2026
0d099b7
feat: spell out team deletion consequences in delete dialog
rohan-chaturvedi Apr 18, 2026
4d47779
fix: rewrite SCIM filter split with lookarounds to eliminate ReDoS pa…
rohan-chaturvedi Apr 18, 2026
721a283
chore: renumber team and scim migrations to follow main's auth chain
rohan-chaturvedi May 1, 2026
d1a3422
fix: use userContext useSession in account-init page after NextAuth r…
rohan-chaturvedi May 1, 2026
9c4aae5
chore: regenerate GraphQL schema and codegen for merged main + team t…
rohan-chaturvedi May 1, 2026
e3859ca
fix: set deviceKey on scim account init, enforce sso for scim users
rohan-chaturvedi May 5, 2026
2ac1a4b
feat: enforce SSO at middleware for SCIM-managed members
rohan-chaturvedi May 5, 2026
b1e1026
feat: auto-link SSO identity to existing user when SCIM-authorised
rohan-chaturvedi May 5, 2026
2cb572a
fix: skip identity_key proof on first-key-ceremony for SCIM members
rohan-chaturvedi May 5, 2026
c7841be
fix: redirect SCIM-restricted access to lobby instead of forcing logout
rohan-chaturvedi May 5, 2026
6089b9e
Merge branch 'main' into feat--teams
rohan-chaturvedi May 5, 2026
783a418
fix: store env identity_key on team-provisioned EnvironmentKeys
rohan-chaturvedi May 5, 2026
be9f97c
feat: gate SCIM SSO auto-link on email_verified claim
rohan-chaturvedi May 5, 2026
155ddc5
feat: gate social adapter auto-link on email_verified
rohan-chaturvedi May 5, 2026
cf53d82
fix: scope TeamType nested resolvers to caller-accessible apps
rohan-chaturvedi May 5, 2026
24b278b
fix: pre-flight per-SA permission check before bulk handler delete
rohan-chaturvedi May 5, 2026
e6c007e
fix: preserve team grants when updating direct env scope
rohan-chaturvedi May 5, 2026
022e107
fix: track grants and preserve team access in app member mutations
rohan-chaturvedi May 5, 2026
357912e
feat: surface env access source on app members page
rohan-chaturvedi May 5, 2026
f66a041
chore: trim verbose comment blocks on auth and team mutations
rohan-chaturvedi May 5, 2026
3aadbcd
fix: apply team role override to top-level Apps actions on app pages
rohan-chaturvedi May 6, 2026
959b4ca
fix: read env from request.auth to avoid 500 on missing Environment h…
rohan-chaturvedi May 6, 2026
4d9543b
fix: block role changes for pending members until key ceremony
rohan-chaturvedi May 6, 2026
5adc50c
fix: copy
rohan-chaturvedi May 6, 2026
1b8f0f3
fix: wrap app member add in atomic to avoid stale m2m on failure
rohan-chaturvedi May 6, 2026
d43ddad
feat: lock team-granted envs in app member and account scope pickers
rohan-chaturvedi May 6, 2026
a89a45d
fix: guard null org members and service accounts in app add dialogs
rohan-chaturvedi May 6, 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
2 changes: 1 addition & 1 deletion .github/workflows/backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: '3.12'
cache: 'pip'

- name: Install dependencies
Expand Down
20 changes: 7 additions & 13 deletions backend/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,24 +164,18 @@ def authenticate(self, request):
raise exceptions.AuthenticationFailed(
"Service account cannot access this environment"
)
except (exceptions.AuthenticationFailed, exceptions.NotFound):
raise # Let DRF exceptions propagate with their specific messages
except Exception as ex:
# Distinguish between ServiceAccount not found and other potential errors
logger.debug(f"ServiceAccount authentication error: {ex}")
# Distinguish between ServiceAccount not found and other errors
ServiceAccount = apps.get_model("api", "ServiceAccount")
try:
# Attempt to get the service account again to confirm if it exists
get_service_account_from_token(auth_token)
# If it exists, the error was likely the environment access check
raise exceptions.AuthenticationFailed(
"Service account cannot access this environment"
)
except ServiceAccount.DoesNotExist:
raise exceptions.NotFound("Service account not found")
except (
Exception
) as ex: # Catch any other unexpected error during the re-check
logger.debug(f"Authentication error: {ex}")
raise exceptions.AuthenticationFailed(
f"Authentication error. Please check your authentication token or App / Environment access."
)
raise exceptions.AuthenticationFailed(
"Authentication error. Please check your authentication token or App / Environment access."
)

return (user, auth)
67 changes: 67 additions & 0 deletions backend/api/authentication/adapters/social.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import logging

from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth.socialaccount.models import SocialAccount
from django.contrib.auth import get_user_model

logger = logging.getLogger(__name__)

User = get_user_model()


class AutoLinkSocialAccountAdapter(DefaultSocialAccountAdapter):
"""
Custom SocialAccountAdapter that automatically links social logins
to existing users with matching email addresses.

This is essential for SCIM-provisioned users: SCIM creates the
CustomUser + OrganisationMember but no SocialAccount. Without this
adapter, the first SSO login would fail with "User is already
registered with this e-mail address."
"""

def pre_social_login(self, request, sociallogin):
# If the social account is already linked, nothing to do
if sociallogin.is_existing:
return

email = sociallogin.user.email
if not email:
return

# Defense-in-depth: never auto-link when the IdP explicitly
# marks the email unverified. Mirrors the SSO callback's gate.
extra_data = sociallogin.account.extra_data or {}
if extra_data.get("email_verified") is False:
logger.warning(
f"Refused auto-link: provider={sociallogin.account.provider} "
f"email={email} not verified by IdP."
)
return

try:
existing_user = User.objects.get(email=email)
except User.DoesNotExist:
return

# Link the social account to the existing user
sociallogin.user = existing_user
social_account, created = SocialAccount.objects.get_or_create(
provider=sociallogin.account.provider,
uid=sociallogin.account.uid,
defaults={
"user": existing_user,
"extra_data": sociallogin.account.extra_data,
},
)

if not created:
social_account.extra_data = sociallogin.account.extra_data
social_account.save()

sociallogin.account = social_account

logger.info(
f"Auto-linked social account ({sociallogin.account.provider}) "
f"to existing user {email}"
)
73 changes: 73 additions & 0 deletions backend/api/migrations/0123_add_team_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Generated by Django 4.2.29 on 2026-03-30 11:38

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


class Migration(migrations.Migration):

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

operations = [
migrations.CreateModel(
name='Team',
fields=[
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('description', models.TextField(blank=True, null=True)),
('is_scim_managed', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_teams', to='api.organisationmember')),
('member_role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teams_as_member_role', to='api.role')),
('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teams', to='api.organisation')),
('service_account_role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teams_as_sa_role', to='api.role')),
],
),
migrations.CreateModel(
name='TeamMembership',
fields=[
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
('org_member', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='team_memberships', to='api.organisationmember')),
('service_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='team_memberships', to='api.serviceaccount')),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='api.team')),
],
),
migrations.CreateModel(
name='TeamAppEnvironment',
fields=[
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.app')),
('environment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.environment')),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='app_environments', to='api.team')),
],
),
migrations.CreateModel(
name='EnvironmentKeyGrant',
fields=[
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('grant_type', models.CharField(choices=[('individual', 'Individual'), ('team', 'Team')], max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
('environment_key', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='grants', to='api.environmentkey')),
('team', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='key_grants', to='api.team')),
],
),
migrations.AddConstraint(
model_name='teammembership',
constraint=models.UniqueConstraint(condition=models.Q(('org_member__isnull', False)), fields=('team', 'org_member'), name='unique_team_user'),
),
migrations.AddConstraint(
model_name='teammembership',
constraint=models.UniqueConstraint(condition=models.Q(('service_account__isnull', False)), fields=('team', 'service_account'), name='unique_team_sa'),
),
migrations.AddConstraint(
model_name='teamappenvironment',
constraint=models.UniqueConstraint(fields=('team', 'environment'), name='unique_team_env'),
),
]
46 changes: 46 additions & 0 deletions backend/api/migrations/0124_backfill_environment_key_grants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Generated by Django 4.2.29 on 2026-03-30 11:38

from django.db import migrations


def backfill_grants(apps, schema_editor):
"""
Create an 'individual' EnvironmentKeyGrant for every existing EnvironmentKey.
This ensures the grant-tracking system works from day one — all pre-existing
keys are attributed to individual access.
"""
EnvironmentKey = apps.get_model("api", "EnvironmentKey")
EnvironmentKeyGrant = apps.get_model("api", "EnvironmentKeyGrant")

grants_to_create = []
for ek in EnvironmentKey.objects.filter(deleted_at__isnull=True).iterator():
grants_to_create.append(
EnvironmentKeyGrant(
environment_key=ek,
grant_type="individual",
team=None,
)
)
if len(grants_to_create) >= 1000:
EnvironmentKeyGrant.objects.bulk_create(grants_to_create, ignore_conflicts=True)
grants_to_create = []

if grants_to_create:
EnvironmentKeyGrant.objects.bulk_create(grants_to_create, ignore_conflicts=True)


def reverse_backfill(apps, schema_editor):
"""Remove all individual grants created by the forward migration."""
EnvironmentKeyGrant = apps.get_model("api", "EnvironmentKeyGrant")
EnvironmentKeyGrant.objects.filter(grant_type="individual", team__isnull=True).delete()


class Migration(migrations.Migration):

dependencies = [
("api", "0123_add_team_models"),
]

operations = [
migrations.RunPython(backfill_grants, reverse_backfill),
]
121 changes: 121 additions & 0 deletions backend/api/migrations/0125_teams_and_scim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Generated by Django 4.2.29 on 2026-04-14 15:57

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):

dependencies = [
('api', '0124_backfill_environment_key_grants'),
]

operations = [
migrations.AddField(
model_name='organisation',
name='scim_enabled',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='serviceaccount',
name='team',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_service_accounts', to='api.team'),
),
migrations.AddField(
model_name='team',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_teams', to='api.organisationmember'),
),
migrations.CreateModel(
name='SCIMUser',
fields=[
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('external_id', models.CharField(max_length=255)),
('email', models.EmailField(max_length=254)),
('display_name', models.CharField(blank=True, max_length=255)),
('active', models.BooleanField(default=True)),
('scim_data', models.JSONField(default=dict)),
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('org_member', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')),
('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scim_users', to='api.organisation')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='SCIMToken',
fields=[
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('token_hash', models.CharField(db_index=True, max_length=128, unique=True)),
('token_prefix', models.CharField(max_length=12)),
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
('expires_at', models.DateTimeField(blank=True, null=True)),
('last_used_at', models.DateTimeField(blank=True, null=True)),
('is_active', models.BooleanField(default=True)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.organisationmember')),
('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scim_tokens', to='api.organisation')),
],
),
migrations.CreateModel(
name='SCIMGroup',
fields=[
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('external_id', models.CharField(max_length=255)),
('display_name', models.CharField(max_length=255)),
('scim_data', models.JSONField(default=dict)),
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scim_groups', to='api.organisation')),
('team', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scim_group', to='api.team')),
],
),
migrations.CreateModel(
name='SCIMEvent',
fields=[
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('event_type', models.CharField(choices=[('user_created', 'User Created'), ('user_updated', 'User Updated'), ('user_deactivated', 'User Deactivated'), ('user_reactivated', 'User Reactivated'), ('group_created', 'Group Created'), ('group_updated', 'Group Updated'), ('group_deleted', 'Group Deleted'), ('member_added', 'Member Added to Group'), ('member_removed', 'Member Removed from Group')], max_length=32)),
('status', models.CharField(choices=[('success', 'Success'), ('error', 'Error')], default='success', max_length=8)),
('resource_type', models.CharField(choices=[('user', 'User'), ('group', 'Group')], max_length=16)),
('resource_id', models.TextField(blank=True)),
('resource_name', models.CharField(blank=True, max_length=255)),
('detail', models.JSONField(default=dict)),
('request_method', models.CharField(blank=True, max_length=8)),
('request_path', models.TextField(blank=True)),
('request_body', models.JSONField(blank=True, null=True)),
('response_status', models.IntegerField(blank=True, null=True)),
('response_body', models.JSONField(blank=True, null=True)),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('user_agent', models.TextField(blank=True, null=True)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scim_events', to='api.organisation')),
('scim_token', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='events', to='api.scimtoken')),
],
options={
'ordering': ['-timestamp'],
},
),
migrations.AddConstraint(
model_name='scimuser',
constraint=models.UniqueConstraint(fields=('external_id', 'organisation'), name='unique_scim_user_external_id'),
),
migrations.AddConstraint(
model_name='scimuser',
constraint=models.UniqueConstraint(fields=('email', 'organisation'), name='unique_scim_user_email'),
),
migrations.AddConstraint(
model_name='scimgroup',
constraint=models.UniqueConstraint(fields=('external_id', 'organisation'), name='unique_scim_group_external_id'),
),
migrations.AddIndex(
model_name='scimevent',
index=models.Index(fields=['organisation', '-timestamp'], name='scim_event_org_ts_idx'),
),
migrations.AddIndex(
model_name='scimevent',
index=models.Index(fields=['scim_token', '-timestamp'], name='scim_event_token_ts_idx'),
),
]
25 changes: 25 additions & 0 deletions backend/api/migrations/0126_populate_team_owner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from django.db import migrations, models


def populate_team_owner(apps, schema_editor):
"""Set owner = created_by for all existing teams."""
Team = apps.get_model("api", "Team")
Team.objects.filter(owner__isnull=True, created_by__isnull=False).update(
owner=models.F("created_by")
)


def reverse(apps, schema_editor):
"""No-op reverse — owner data is additive."""
pass


class Migration(migrations.Migration):

dependencies = [
('api', '0125_teams_and_scim'),
]

operations = [
migrations.RunPython(populate_team_owner, reverse),
]
Loading
Loading