Skip to content

Commit 26b92d6

Browse files
Merge pull request #115 from phasehq/feat--personal-secrets
feat: personal secrets
2 parents 7181cc0 + f6c6838 commit 26b92d6

File tree

19 files changed

+618
-43
lines changed

19 files changed

+618
-43
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 4.2.3 on 2023-11-14 06:58
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import uuid
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('api', '0038_secretevent_ip_address_secretevent_user_agent'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='PersonalSecret',
17+
fields=[
18+
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
19+
('value', models.TextField()),
20+
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
21+
('updated_at', models.DateTimeField(auto_now=True)),
22+
('deleted_at', models.DateTimeField(blank=True, null=True)),
23+
('secret', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.secret')),
24+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')),
25+
],
26+
),
27+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.2.3 on 2023-11-16 05:46
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api', '0039_personalsecret'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='personalsecret',
15+
name='isActive',
16+
field=models.BooleanField(default=True),
17+
),
18+
migrations.AlterField(
19+
model_name='personalsecret',
20+
name='value',
21+
field=models.TextField(blank=True, null=True),
22+
),
23+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.3 on 2023-11-16 05:53
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api', '0040_personalsecret_isactive_alter_personalsecret_value'),
10+
]
11+
12+
operations = [
13+
migrations.RenameField(
14+
model_name='personalsecret',
15+
old_name='isActive',
16+
new_name='is_active',
17+
),
18+
]

backend/api/models.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,15 @@ class SecretEvent(models.Model):
341341
timestamp = models.DateTimeField(auto_now_add=True)
342342
ip_address = models.GenericIPAddressField(null=True, blank=True)
343343
user_agent = models.TextField(null=True, blank=True)
344+
345+
346+
class PersonalSecret(models.Model):
347+
id = models.TextField(default=uuid4, primary_key=True, editable=False)
348+
secret = models.ForeignKey(Secret, on_delete=models.CASCADE)
349+
user = models.ForeignKey(
350+
OrganisationMember, on_delete=models.CASCADE)
351+
value = models.TextField(blank=True, null=True)
352+
is_active = models.BooleanField(default=True)
353+
created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)
354+
updated_at = models.DateTimeField(auto_now=True)
355+
deleted_at = models.DateTimeField(blank=True, null=True)

backend/api/serializers.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from rest_framework import serializers
2-
from .models import CustomUser, Environment, EnvironmentKey, Organisation, Secret, ServiceToken, UserToken
2+
from .models import CustomUser, Environment, EnvironmentKey, Organisation, Secret, ServiceToken, UserToken, PersonalSecret
33

44

55
def find_index_by_id(dictionaries, target_id):
@@ -38,11 +38,32 @@ def create(self, validated_data):
3838
return Organisation(**validated_data)
3939

4040

41+
class PersonalSecretSerializer(serializers.ModelSerializer):
42+
class Meta:
43+
model = PersonalSecret
44+
fields = '__all__'
45+
46+
4147
class SecretSerializer(serializers.ModelSerializer):
48+
override = serializers.SerializerMethodField()
49+
4250
class Meta:
4351
model = Secret
4452
fields = '__all__'
4553

54+
def get_override(self, obj):
55+
# Assuming 'request' is passed to the context of the serializer.
56+
org_member = self.context.get('org_member')
57+
if org_member:
58+
59+
try:
60+
personal_secret = PersonalSecret.objects.get(
61+
secret=obj, user=org_member)
62+
return PersonalSecretSerializer(personal_secret).data
63+
except PersonalSecret.DoesNotExist:
64+
return None
65+
return None
66+
4667

4768
class EnvironmentSerializer(serializers.ModelSerializer):
4869
class Meta:

backend/api/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,8 @@ def get(self, request):
346346
value=secret.value, comment=secret.comment, event_type=SecretEvent.READ, ip_address=ip_address, user_agent=user_agent)
347347
read_event.tags.set(secret.tags.all())
348348

349-
serializer = SecretSerializer(secrets, many=True)
349+
serializer = SecretSerializer(secrets, many=True, context={
350+
'org_member': org_member})
350351

351352
return Response(serializer.data, status=status.HTTP_200_OK)
352353

backend/backend/graphene/mutations/environment.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from backend.graphene.utils.permissions import member_can_access_org, user_can_access_app, user_can_access_environment, user_is_org_member
44
import graphene
55
from graphql import GraphQLError
6-
from api.models import App, Environment, EnvironmentKey, EnvironmentToken, Organisation, OrganisationMember, Secret, SecretEvent, SecretFolder, SecretTag, UserToken, ServiceToken
7-
from backend.graphene.types import AppType, EnvironmentKeyType, EnvironmentTokenType, EnvironmentType, SecretFolderType, SecretTagType, SecretType, ServiceTokenType, UserTokenType
6+
from api.models import App, Environment, EnvironmentKey, EnvironmentToken, Organisation, OrganisationMember, PersonalSecret, Secret, SecretEvent, SecretFolder, SecretTag, UserToken, ServiceToken
7+
from backend.graphene.types import AppType, EnvironmentKeyType, EnvironmentTokenType, EnvironmentType, PersonalSecretType, SecretFolderType, SecretTagType, SecretType, ServiceTokenType, UserTokenType
88
from datetime import datetime
99

1010

@@ -35,6 +35,12 @@ class SecretInput(graphene.InputObjectType):
3535
comment = graphene.String()
3636

3737

38+
class PersonalSecretInput(graphene.InputObjectType):
39+
secret_id = graphene.ID()
40+
value = graphene.String()
41+
is_active = graphene.Boolean()
42+
43+
3844
class CreateEnvironmentMutation(graphene.Mutation):
3945
class Arguments:
4046
environment_data = EnvironmentInput(required=True)
@@ -488,3 +494,54 @@ def mutate(cls, root, info, id):
488494
value=secret.value, comment=secret.comment, event_type=SecretEvent.READ, ip_address=ip_address, user_agent=user_agent)
489495
read_event.tags.set(secret.tags.all())
490496
return ReadSecretMutation(ok=True)
497+
498+
499+
class CreatePersonalSecretMutation(graphene.Mutation):
500+
501+
class Arguments:
502+
override_data = PersonalSecretInput(PersonalSecretInput)
503+
504+
override = graphene.Field(PersonalSecretType)
505+
506+
@classmethod
507+
def mutate(cls, root, info, override_data):
508+
secret = Secret.objects.get(id=override_data.secret_id)
509+
org = secret.environment.app.organisation
510+
org_member = OrganisationMember.objects.get(
511+
organisation=org, user=info.context.user)
512+
513+
if not user_can_access_environment(info.context.user, secret.environment.id):
514+
raise GraphQLError(
515+
"You don't have access to this secret")
516+
517+
override, created = PersonalSecret.objects.get_or_create(
518+
secret_id=override_data.secret_id, user=org_member)
519+
override.value = override_data.value
520+
override.is_active = override_data.is_active
521+
override.save()
522+
523+
return CreatePersonalSecretMutation(override=override)
524+
525+
526+
class DeletePersonalSecretMutation(graphene.Mutation):
527+
528+
class Arguments:
529+
secret_id = graphene.ID()
530+
531+
ok = graphene.Boolean()
532+
533+
@classmethod
534+
def mutate(cls, root, info, secret_id):
535+
secret = Secret.objects.get(id=secret_id)
536+
org = secret.environment.app.organisation
537+
org_member = OrganisationMember.objects.get(
538+
organisation=org, user=info.context.user)
539+
540+
if not user_can_access_environment(info.context.user, secret.environment.id):
541+
raise GraphQLError(
542+
"You don't have access to this secret")
543+
544+
PersonalSecret.objects.filter(
545+
secret_id=secret_id, user=org_member).delete()
546+
547+
return DeletePersonalSecretMutation(ok=True)

backend/backend/graphene/types.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from enum import Enum
33
from graphene import ObjectType, relay
44
from graphene_django import DjangoObjectType
5-
from api.models import CustomUser, Environment, EnvironmentKey, EnvironmentToken, Organisation, App, OrganisationMember, OrganisationMemberInvite, Secret, SecretEvent, SecretFolder, SecretTag, ServiceToken, UserToken
5+
from api.models import CustomUser, Environment, EnvironmentKey, EnvironmentToken, Organisation, App, OrganisationMember, OrganisationMemberInvite, PersonalSecret, Secret, SecretEvent, SecretFolder, SecretTag, ServiceToken, UserToken
66
from logs.dynamodb_models import KMSLog
77
from allauth.socialaccount.models import SocialAccount
88

@@ -165,19 +165,42 @@ class Meta:
165165
'version', 'tags', 'comment', 'event_type', 'timestamp', 'user', 'ip_address', 'user_agent', 'environment')
166166

167167

168+
class PersonalSecretType(DjangoObjectType):
169+
class Meta:
170+
model = PersonalSecret
171+
fields = ('id', 'secret', 'user', 'value',
172+
'is_active', 'created_at', 'updated_at')
173+
174+
168175
class SecretType(DjangoObjectType):
169176

170177
history = graphene.List(SecretEventType)
178+
override = graphene.Field(PersonalSecretType)
171179

172180
class Meta:
173181
model = Secret
174182
fields = ('id', 'key', 'value', 'folder', 'version', 'tags',
175-
'comment', 'created_at', 'updated_at', 'history')
183+
'comment', 'created_at', 'updated_at', 'history', 'override')
176184
# interfaces = (relay.Node, )
177185

178186
def resolve_history(self, info):
179187
return SecretEvent.objects.filter(secret_id=self.id, event_type__in=[SecretEvent.CREATE, SecretEvent.UPDATE]).order_by('timestamp')
180188

189+
def resolve_override(self, info):
190+
if info.context.user:
191+
org = self.environment.app.organisation
192+
org_member = OrganisationMember.objects.get(
193+
organisation=org, user=info.context.user)
194+
195+
try:
196+
override = PersonalSecret.objects.get(
197+
secret=self, user=org_member)
198+
199+
if override is not None:
200+
return override
201+
except:
202+
return None
203+
181204

182205
class KMSLogType(ObjectType):
183206
class Meta:

backend/backend/schema.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .graphene.mutations.environment import CreateEnvironmentKeyMutation, CreateEnvironmentMutation, CreateEnvironmentTokenMutation, CreateSecretFolderMutation, CreateSecretMutation, CreateSecretTagMutation, CreateServiceTokenMutation, CreateUserTokenMutation, DeleteSecretMutation, DeleteServiceTokenMutation, DeleteUserTokenMutation, EditSecretMutation, ReadSecretMutation, UpdateMemberEnvScopeMutation
1+
from .graphene.mutations.environment import CreateEnvironmentKeyMutation, CreateEnvironmentMutation, CreateEnvironmentTokenMutation, CreatePersonalSecretMutation, CreateSecretFolderMutation, CreateSecretMutation, CreateSecretTagMutation, CreateServiceTokenMutation, CreateUserTokenMutation, DeletePersonalSecretMutation, DeleteSecretMutation, DeleteServiceTokenMutation, DeleteUserTokenMutation, EditSecretMutation, ReadSecretMutation, UpdateMemberEnvScopeMutation
22
from .graphene.utils.permissions import user_can_access_app, user_can_access_environment, user_is_admin, user_is_org_member
33
from .graphene.mutations.app import AddAppMemberMutation, CreateAppMutation, DeleteAppMutation, RemoveAppMemberMutation, RotateAppKeysMutation
44
from .graphene.mutations.organisation import CreateOrganisationMemberMutation, CreateOrganisationMutation, DeleteInviteMutation, DeleteOrganisationMemberMutation, InviteOrganisationMemberMutation, UpdateOrganisationMemberRole, UpdateUserWrappedSecretsMutation
@@ -409,6 +409,8 @@ class Mutation(graphene.ObjectType):
409409
edit_secret = EditSecretMutation.Field()
410410
delete_secret = DeleteSecretMutation.Field()
411411
read_secret = ReadSecretMutation.Field()
412+
create_override = CreatePersonalSecretMutation.Field()
413+
remove_override = DeletePersonalSecretMutation.Field()
412414

413415

414416
schema = graphene.Schema(query=Query, mutation=Mutation)

0 commit comments

Comments
 (0)