Skip to content

Commit

Permalink
feat: service accounts (#383)
Browse files Browse the repository at this point in the history
* feat: add service account models, types, resolvers and schema

* chore: regenerate graphql schema

* feat: add various query and mutation resolvers

* fix: hard delete service accounts

* chore: regenerate schema and types

* feat: add client queries

* feat: add service account page, create and delete dialogs

* feat: add new service account token model

* feat: allow fetching single service account by id

* feat: add sa token create mutation

* feat: add client side queries

* chore: regenerate graphql schema and types

* feat: add service account management page, misc changes and cleanup

* feat: delete service account tokens

* fix: related name for service accounts app fk

* fix: add service accounts to app permissions

* refactor: app access

* chore: update operation names to avoid type collisions

* feat: add service accounts to app card

* feat: service account auth, permissions serializers for rest

* feat: add service account logging for secret crud

* feat: render service accounts in logs

* fix: check that service account has handlers

* fix: spelling

* test: permission check

* fix: typo

* fix: permission checks

* fix: typo

* feat: manage service account handler updates when updating, assigning roles

* fix: permission check logic for pats

* feat: add service account tokens permissions class

* chore: bump deps for python 3.13 support

* feat: add managed manager and service roles

* feat: add service account role selector

* feat: add service as the default role for service accounts

* feat: add client mutation to update service accounts

* feat: restrict service accounts from global access roles

* feat: add role selector to service account manage page

* feat: sort roles to have  managed roles first

* feat: managed role label styles

* feat: add ui to update account name from account management screen

* feat: add link to account tokens to app access

* fix: enforce permissions for account property updates

* fix: reset create account dialog after success

* feat: add quota logic for service accounts

* fix: copy

* fix: update app settings danger zone style for consistency

* feat: misc tweaks to tab ui

* fix: command palette commands

* fix: misc fixes and updates to navbar

* fix: misc styling fixes

* feat: update stripe subscription count when adding or removing service accounts

* feat: update acccount management ui

* feat: add warning for legacy service tokens

* fix: misc ui tweaks

* fix: add empty state for service accounts

* fix: overflow in create account dialog

* fix: add button link to access when all accounts added to app

* fix: query updated fields during service account update

* chore: regenerate gql types

* typo

* Feat  service accounts service role description (#387)

* feat: updated managed service role description

* feat: improved owner role description

* feat: updated admin role description

* feat: updated manager role description

* feat: updated developer role description

* feat: updated service role description

* fix: env access check for service account

* feat: sort managed roles

* feat: add app memberships to service account page

* feat: log and display service account token usage

* fix: misc fixes to service account app memberships

* fix: update env scope display

* feat: link to service account from secret logs

* fix: command palette duplicate id

* fix: access template listbox positioning

* fix: add legacy label to service tokens in role manager

* fix: tweak listbox open state styling, position

* fix: misc fixes to rest api permission checking

* refactor: move api permission checks to middleware

* chore: regenerate types

* feat: add empty state for service account app membership

* fix: merge migrations

* refactor: rest permissions

* fix: exception response for 403s

* fix: last used resolver

* fix: get role color

* fix: invite user link in command palette

* chore: regenerate types

* fix: only render "danger zone" if delete allowed

* fix: get started onboarding link

* fix: log row border width, alignment

* Feat: collapsible sidebar (#389)

* feat: added sidebar state local storage context util

* feat: collapsible sidebar

* feat: sidebar provider in providers

* fix: misc updates to smooth transitions between states

* feat: misc tweaks to orgs menu

* fix: load stored state after component mount to prevent hydration mismatch

---------

Co-authored-by: Rohan <[email protected]>

* feat: update seat limits and usage (#393)

* feat: consolidate seat quota logic

* feat: misc updates to plan info display

* refactor: org seat usage types

* fix: misc copy and ui updates

* feat: remove feature list on self-hosted

* chore: bumped version

* fix: token label

* fix: remove the inaccurate token access note

---------

Co-authored-by: Nimish <[email protected]>
Co-authored-by: Nimish <[email protected]>
  • Loading branch information
3 people authored Nov 24, 2024
1 parent cf2db1b commit 4a9920d
Show file tree
Hide file tree
Showing 88 changed files with 5,419 additions and 762 deletions.
37 changes: 33 additions & 4 deletions backend/api/auth.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
from api.utils.rest import (
get_org_member_from_user_token,
get_service_account_from_token,
get_service_token,
get_token_type,
token_is_expired_or_deleted,
)
from api.models import Environment
from api.utils.access.permissions import user_can_access_environment
from api.utils.access.permissions import (
service_account_can_access_environment,
user_can_access_environment,
)
from rest_framework import authentication, exceptions


class PhaseTokenAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):

token_types = ["User", "Service"]
token_types = ["User", "Service", "ServiceAccount"]

auth_token = request.headers.get("Authorization")

Expand All @@ -24,7 +28,14 @@ def authenticate(self, request):
if token_type not in token_types:
raise exceptions.AuthenticationFailed("Invalid token")

auth = {"token": auth_token, "org_member": None, "service_token": None}
auth = {
"token": auth_token,
"auth_type": token_type,
"org_member": None,
"service_token": None,
"service_account": None,
"service_account_token": None,
}

if token_is_expired_or_deleted(auth_token):
raise exceptions.AuthenticationFailed("Token expired or deleted")
Expand Down Expand Up @@ -62,9 +73,27 @@ def authenticate(self, request):
except Exception as ex:
raise exceptions.NotFound("User not found")

else:
elif token_type == "Service":
service_token = get_service_token(auth_token)
auth["service_token"] = service_token
user = service_token.created_by.user

if token_type == "ServiceAccount":

try:
service_token = get_service_token(auth_token)
service_account = get_service_account_from_token(auth_token)
user = service_token.created_by.user
auth["service_account"] = service_account
auth["service_account_token"] = service_token

if not service_account_can_access_environment(
service_account.id, env.id
):
raise exceptions.AuthenticationFailed(
"Service account cannot access this environment"
)
except Exception as ex:
raise exceptions.NotFound("Service account not found")

return (user, auth)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Generated by Django 4.2.15 on 2024-10-20 09:12

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


class Migration(migrations.Migration):

dependencies = [
('api', '0084_auto_20241008_0708'),
]

operations = [
migrations.CreateModel(
name='ServiceAccount',
fields=[
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('identity_key', models.CharField(blank=True, max_length=256, null=True)),
('server_wrapped_keyring', models.TextField(null=True)),
('server_wrapped_recovery', models.TextField(null=True)),
('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)),
('apps', models.ManyToManyField(related_name='apps', to='api.app')),
('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.organisation')),
('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.role')),
],
),
migrations.AddField(
model_name='environmentkey',
name='paths',
field=models.TextField(blank=True, null=True),
),
migrations.CreateModel(
name='ServiceAccountHandler',
fields=[
('id', models.TextField(default=uuid.uuid4, primary_key=True, serialize=False)),
('wrapped_keyring', models.TextField()),
('wrapped_recovery', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('service_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.serviceaccount')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')),
],
),
migrations.AddField(
model_name='environmentkey',
name='service_account',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.serviceaccount'),
),
migrations.AddField(
model_name='servicetoken',
name='service_account',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.serviceaccount'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.2.15 on 2024-10-22 07:29

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


class Migration(migrations.Migration):

dependencies = [
('api', '0085_serviceaccount_environmentkey_paths_and_more'),
]

operations = [
migrations.RemoveField(
model_name='servicetoken',
name='service_account',
),
migrations.CreateModel(
name='ServiceAccountToken',
fields=[
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('identity_key', models.CharField(max_length=256)),
('token', models.CharField(max_length=64)),
('wrapped_key_share', models.CharField(max_length=406)),
('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)),
('expires_at', models.DateTimeField(null=True)),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')),
('service_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.serviceaccount')),
],
),
]
18 changes: 18 additions & 0 deletions backend/api/migrations/0087_alter_serviceaccount_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.15 on 2024-10-24 12:31

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0086_remove_servicetoken_service_account_and_more'),
]

operations = [
migrations.AlterField(
model_name='serviceaccount',
name='apps',
field=models.ManyToManyField(related_name='service_accounts', to='api.app'),
),
]
19 changes: 19 additions & 0 deletions backend/api/migrations/0088_secretevent_service_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.15 on 2024-10-28 08:43

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


class Migration(migrations.Migration):

dependencies = [
('api', '0087_alter_serviceaccount_apps'),
]

operations = [
migrations.AddField(
model_name='secretevent',
name='service_account',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.serviceaccount'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.15 on 2024-10-30 07:10

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


class Migration(migrations.Migration):

dependencies = [
('api', '0088_secretevent_service_account'),
]

operations = [
migrations.AlterField(
model_name='serviceaccounthandler',
name='service_account',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='handlers', to='api.serviceaccount'),
),
]
19 changes: 19 additions & 0 deletions backend/api/migrations/0090_alter_serviceaccount_organisation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.15 on 2024-10-30 07:11

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


class Migration(migrations.Migration):

dependencies = [
('api', '0089_alter_serviceaccounthandler_service_account'),
]

operations = [
migrations.AlterField(
model_name='serviceaccount',
name='organisation',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_accounts', to='api.organisation'),
),
]
30 changes: 30 additions & 0 deletions backend/api/migrations/0091_add_managed_manager_service_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 4.2.15 on 2024-11-04 08:36

from django.db import migrations
from api.utils.access.roles import default_roles


def add_default_roles(apps, schema_editor):
Organisation = apps.get_model("api", "Organisation")
Role = apps.get_model("api", "Role")
OrganisationMember = apps.get_model("api", "OrganisationMember")

# Create default roles for each organisation
for organisation in Organisation.objects.all():
for role_name, _ in default_roles.items():
Role.objects.get_or_create(
name=role_name,
organisation=organisation,
is_default=True,
)


class Migration(migrations.Migration):

dependencies = [
("api", "0090_alter_serviceaccount_organisation"),
]

operations = [
migrations.RunPython(add_default_roles),
]
19 changes: 19 additions & 0 deletions backend/api/migrations/0092_secretevent_service_account_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.15 on 2024-11-13 14:47

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


class Migration(migrations.Migration):

dependencies = [
('api', '0091_add_managed_manager_service_roles'),
]

operations = [
migrations.AddField(
model_name='secretevent',
name='service_account_token',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.serviceaccounttoken'),
),
]
14 changes: 14 additions & 0 deletions backend/api/migrations/0093_merge_20241116_0744.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 4.2.15 on 2024-11-16 07:44

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('api', '0086_alter_environmentsync_service'),
('api', '0092_secretevent_service_account_token'),
]

operations = [
]
Loading

0 comments on commit 4a9920d

Please sign in to comment.