Skip to content
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

feat: personal secrets #115

Merged
merged 18 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions backend/api/migrations/0039_personalsecret.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.2.3 on 2023-11-14 06:58

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


class Migration(migrations.Migration):

dependencies = [
('api', '0038_secretevent_ip_address_secretevent_user_agent'),
]

operations = [
migrations.CreateModel(
name='PersonalSecret',
fields=[
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('value', models.TextField()),
('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)),
('secret', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.secret')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')),
],
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.3 on 2023-11-16 05:46

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0039_personalsecret'),
]

operations = [
migrations.AddField(
model_name='personalsecret',
name='isActive',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='personalsecret',
name='value',
field=models.TextField(blank=True, null=True),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.3 on 2023-11-16 05:53

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('api', '0040_personalsecret_isactive_alter_personalsecret_value'),
]

operations = [
migrations.RenameField(
model_name='personalsecret',
old_name='isActive',
new_name='is_active',
),
]
12 changes: 12 additions & 0 deletions backend/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,15 @@ class SecretEvent(models.Model):
timestamp = models.DateTimeField(auto_now_add=True)
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(null=True, blank=True)


class PersonalSecret(models.Model):
id = models.TextField(default=uuid4, primary_key=True, editable=False)
secret = models.ForeignKey(Secret, on_delete=models.CASCADE)
user = models.ForeignKey(
OrganisationMember, on_delete=models.CASCADE)
value = models.TextField(blank=True, null=True)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)
updated_at = models.DateTimeField(auto_now=True)
deleted_at = models.DateTimeField(blank=True, null=True)
23 changes: 22 additions & 1 deletion backend/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from rest_framework import serializers
from .models import CustomUser, Environment, EnvironmentKey, Organisation, Secret, ServiceToken, UserToken
from .models import CustomUser, Environment, EnvironmentKey, Organisation, Secret, ServiceToken, UserToken, PersonalSecret


def find_index_by_id(dictionaries, target_id):
Expand Down Expand Up @@ -38,11 +38,32 @@ def create(self, validated_data):
return Organisation(**validated_data)


class PersonalSecretSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalSecret
fields = '__all__'


class SecretSerializer(serializers.ModelSerializer):
override = serializers.SerializerMethodField()

class Meta:
model = Secret
fields = '__all__'

def get_override(self, obj):
# Assuming 'request' is passed to the context of the serializer.
org_member = self.context.get('org_member')
if org_member:

try:
personal_secret = PersonalSecret.objects.get(
secret=obj, user=org_member)
return PersonalSecretSerializer(personal_secret).data
except PersonalSecret.DoesNotExist:
return None
return None


class EnvironmentSerializer(serializers.ModelSerializer):
class Meta:
Expand Down
3 changes: 2 additions & 1 deletion backend/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,8 @@ def get(self, request):
value=secret.value, comment=secret.comment, event_type=SecretEvent.READ, ip_address=ip_address, user_agent=user_agent)
read_event.tags.set(secret.tags.all())

serializer = SecretSerializer(secrets, many=True)
serializer = SecretSerializer(secrets, many=True, context={
'org_member': org_member})

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

Expand Down
61 changes: 59 additions & 2 deletions backend/backend/graphene/mutations/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from backend.graphene.utils.permissions import member_can_access_org, user_can_access_app, user_can_access_environment, user_is_org_member
import graphene
from graphql import GraphQLError
from api.models import App, Environment, EnvironmentKey, EnvironmentToken, Organisation, OrganisationMember, Secret, SecretEvent, SecretFolder, SecretTag, UserToken, ServiceToken
from backend.graphene.types import AppType, EnvironmentKeyType, EnvironmentTokenType, EnvironmentType, SecretFolderType, SecretTagType, SecretType, ServiceTokenType, UserTokenType
from api.models import App, Environment, EnvironmentKey, EnvironmentToken, Organisation, OrganisationMember, PersonalSecret, Secret, SecretEvent, SecretFolder, SecretTag, UserToken, ServiceToken
from backend.graphene.types import AppType, EnvironmentKeyType, EnvironmentTokenType, EnvironmentType, PersonalSecretType, SecretFolderType, SecretTagType, SecretType, ServiceTokenType, UserTokenType
from datetime import datetime


Expand Down Expand Up @@ -35,6 +35,12 @@ class SecretInput(graphene.InputObjectType):
comment = graphene.String()


class PersonalSecretInput(graphene.InputObjectType):
secret_id = graphene.ID()
value = graphene.String()
is_active = graphene.Boolean()


class CreateEnvironmentMutation(graphene.Mutation):
class Arguments:
environment_data = EnvironmentInput(required=True)
Expand Down Expand Up @@ -488,3 +494,54 @@ def mutate(cls, root, info, id):
value=secret.value, comment=secret.comment, event_type=SecretEvent.READ, ip_address=ip_address, user_agent=user_agent)
read_event.tags.set(secret.tags.all())
return ReadSecretMutation(ok=True)


class CreatePersonalSecretMutation(graphene.Mutation):

class Arguments:
override_data = PersonalSecretInput(PersonalSecretInput)

override = graphene.Field(PersonalSecretType)

@classmethod
def mutate(cls, root, info, override_data):
secret = Secret.objects.get(id=override_data.secret_id)
org = secret.environment.app.organisation
org_member = OrganisationMember.objects.get(
organisation=org, user=info.context.user)

if not user_can_access_environment(info.context.user, secret.environment.id):
raise GraphQLError(
"You don't have access to this secret")

override, created = PersonalSecret.objects.get_or_create(
secret_id=override_data.secret_id, user=org_member)
override.value = override_data.value
override.is_active = override_data.is_active
override.save()

return CreatePersonalSecretMutation(override=override)


class DeletePersonalSecretMutation(graphene.Mutation):

class Arguments:
secret_id = graphene.ID()

ok = graphene.Boolean()

@classmethod
def mutate(cls, root, info, secret_id):
secret = Secret.objects.get(id=secret_id)
org = secret.environment.app.organisation
org_member = OrganisationMember.objects.get(
organisation=org, user=info.context.user)

if not user_can_access_environment(info.context.user, secret.environment.id):
raise GraphQLError(
"You don't have access to this secret")

PersonalSecret.objects.filter(
secret_id=secret_id, user=org_member).delete()

return DeletePersonalSecretMutation(ok=True)
27 changes: 25 additions & 2 deletions backend/backend/graphene/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from enum import Enum
from graphene import ObjectType, relay
from graphene_django import DjangoObjectType
from api.models import CustomUser, Environment, EnvironmentKey, EnvironmentToken, Organisation, App, OrganisationMember, OrganisationMemberInvite, Secret, SecretEvent, SecretFolder, SecretTag, ServiceToken, UserToken
from api.models import CustomUser, Environment, EnvironmentKey, EnvironmentToken, Organisation, App, OrganisationMember, OrganisationMemberInvite, PersonalSecret, Secret, SecretEvent, SecretFolder, SecretTag, ServiceToken, UserToken
from logs.dynamodb_models import KMSLog
from allauth.socialaccount.models import SocialAccount

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


class PersonalSecretType(DjangoObjectType):
class Meta:
model = PersonalSecret
fields = ('id', 'secret', 'user', 'value',
'is_active', 'created_at', 'updated_at')


class SecretType(DjangoObjectType):

history = graphene.List(SecretEventType)
override = graphene.Field(PersonalSecretType)

class Meta:
model = Secret
fields = ('id', 'key', 'value', 'folder', 'version', 'tags',
'comment', 'created_at', 'updated_at', 'history')
'comment', 'created_at', 'updated_at', 'history', 'override')
# interfaces = (relay.Node, )

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

def resolve_override(self, info):
if info.context.user:
org = self.environment.app.organisation
org_member = OrganisationMember.objects.get(
organisation=org, user=info.context.user)

try:
override = PersonalSecret.objects.get(
secret=self, user=org_member)

if override is not None:
return override
except:
return None


class KMSLogType(ObjectType):
class Meta:
Expand Down
4 changes: 3 additions & 1 deletion backend/backend/schema.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .graphene.mutations.environment import CreateEnvironmentKeyMutation, CreateEnvironmentMutation, CreateEnvironmentTokenMutation, CreateSecretFolderMutation, CreateSecretMutation, CreateSecretTagMutation, CreateServiceTokenMutation, CreateUserTokenMutation, DeleteSecretMutation, DeleteServiceTokenMutation, DeleteUserTokenMutation, EditSecretMutation, ReadSecretMutation, UpdateMemberEnvScopeMutation
from .graphene.mutations.environment import CreateEnvironmentKeyMutation, CreateEnvironmentMutation, CreateEnvironmentTokenMutation, CreatePersonalSecretMutation, CreateSecretFolderMutation, CreateSecretMutation, CreateSecretTagMutation, CreateServiceTokenMutation, CreateUserTokenMutation, DeletePersonalSecretMutation, DeleteSecretMutation, DeleteServiceTokenMutation, DeleteUserTokenMutation, EditSecretMutation, ReadSecretMutation, UpdateMemberEnvScopeMutation
from .graphene.utils.permissions import user_can_access_app, user_can_access_environment, user_is_admin, user_is_org_member
from .graphene.mutations.app import AddAppMemberMutation, CreateAppMutation, DeleteAppMutation, RemoveAppMemberMutation, RotateAppKeysMutation
from .graphene.mutations.organisation import CreateOrganisationMemberMutation, CreateOrganisationMutation, DeleteInviteMutation, DeleteOrganisationMemberMutation, InviteOrganisationMemberMutation, UpdateOrganisationMemberRole, UpdateUserWrappedSecretsMutation
Expand Down Expand Up @@ -409,6 +409,8 @@ class Mutation(graphene.ObjectType):
edit_secret = EditSecretMutation.Field()
delete_secret = DeleteSecretMutation.Field()
read_secret = ReadSecretMutation.Field()
create_override = CreatePersonalSecretMutation.Field()
remove_override = DeletePersonalSecretMutation.Field()


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