Skip to content

Commit

Permalink
refactor: misc optimizations and ux improvements to secrets editing (#…
Browse files Browse the repository at this point in the history
…340)

* feat: add mutation resolvers for bulk secret crud

* feat: add operation to bulk process secret updates

* refactor: bulk process secret updates, pre-stage deletes

* feat: make button loading spinner color match button type

* feat: bulk create example secrets when creating new app

* fix: disable secret edit actions once staged for delete

* fix: remove redundant query fields in mutation

* fix: misc fixes

* fix: make deleted state take priority over edited state, improve light theme colors

* fix: remove extra discard button

* fix: missing filter arg in personal secret mutation

* fix: filter arg when deleting personal secret
  • Loading branch information
rohan-chaturvedi authored Aug 12, 2024
1 parent aaed32e commit 2bc581e
Show file tree
Hide file tree
Showing 10 changed files with 461 additions and 163 deletions.
151 changes: 149 additions & 2 deletions backend/backend/graphene/mutations/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class EnvironmentKeyInput(graphene.InputObjectType):


class SecretInput(graphene.InputObjectType):
id = graphene.ID(required=False)
env_id = graphene.ID(required=False)
path = graphene.String(required=False)
key = graphene.String(required=True)
Expand Down Expand Up @@ -688,6 +689,65 @@ def mutate(cls, root, info, secret_data):
return CreateSecretMutation(secret=secret)


class BulkCreateSecretMutation(graphene.Mutation):
class Arguments:
secrets_data = graphene.List(SecretInput, required=True)

secrets = graphene.List(SecretType)

@classmethod
def mutate(cls, root, info, secrets_data):
created_secrets = []

for secret_data in secrets_data:
env = Environment.objects.get(id=secret_data.env_id)
org = env.app.organisation
if not user_is_org_member(info.context.user.userId, org.id):
raise GraphQLError("You don't have permission to perform this action")

tags = SecretTag.objects.filter(id__in=secret_data.tags)

path = (
normalize_path_string(secret_data.path)
if secret_data.path is not None
else "/"
)

folder = None
if path != "/":
folder_name = path.split("/")[-1]
folder_path, _, _ = path.rpartition("/" + folder_name)
folder_path = folder_path if folder_path else "/"
folder = SecretFolder.objects.get(
environment_id=env.id, path=folder_path, name=folder_name
)

secret_obj_data = {
"environment_id": env.id,
"path": path,
"folder_id": folder.id if folder is not None else None,
"key": secret_data.key,
"key_digest": secret_data.key_digest,
"value": secret_data.value,
"version": 1,
"comment": secret_data.comment,
}

secret = Secret.objects.create(**secret_obj_data)
secret.tags.set(tags)
created_secrets.append(secret)

ip_address, user_agent = get_resolver_request_meta(info.context)
org_member = OrganisationMember.objects.get(
user=info.context.user, organisation=org, deleted_at=None
)
log_secret_event(
secret, SecretEvent.CREATE, org_member, None, ip_address, user_agent
)

return BulkCreateSecretMutation(secrets=created_secrets)


class EditSecretMutation(graphene.Mutation):
class Arguments:
id = graphene.ID(required=True)
Expand Down Expand Up @@ -740,6 +800,59 @@ def mutate(cls, root, info, id, secret_data):
return EditSecretMutation(secret=secret)


class BulkEditSecretMutation(graphene.Mutation):
class Arguments:
secrets_data = graphene.List(SecretInput, required=True)

secrets = graphene.List(SecretType)

@classmethod
def mutate(cls, root, info, secrets_data):
updated_secrets = []

for secret_data in secrets_data:
secret = Secret.objects.get(id=secret_data.id)
env = secret.environment
org = env.app.organisation
if not user_is_org_member(info.context.user.userId, org.id):
raise GraphQLError("You don't have permission to perform this action")

tags = SecretTag.objects.filter(id__in=secret_data.tags)

path = (
normalize_path_string(secret_data.path)
if secret_data.path is not None
else "/"
)

secret_obj_data = {
"path": path,
"key": secret_data.key,
"key_digest": secret_data.key_digest,
"value": secret_data.value,
"version": secret.version + 1,
"comment": secret_data.comment,
}

for key, value in secret_obj_data.items():
setattr(secret, key, value)

secret.updated_at = timezone.now()
secret.tags.set(tags)
secret.save()
updated_secrets.append(secret)

ip_address, user_agent = get_resolver_request_meta(info.context)
org_member = OrganisationMember.objects.get(
user=info.context.user, organisation=org, deleted_at=None
)
log_secret_event(
secret, SecretEvent.UPDATE, org_member, None, ip_address, user_agent
)

return BulkEditSecretMutation(secrets=updated_secrets)


class DeleteSecretMutation(graphene.Mutation):
class Arguments:
id = graphene.ID(required=True)
Expand Down Expand Up @@ -772,6 +885,40 @@ def mutate(cls, root, info, id):
return DeleteSecretMutation(secret=secret)


class BulkDeleteSecretMutation(graphene.Mutation):
class Arguments:
ids = graphene.List(graphene.ID, required=True)

secrets = graphene.List(SecretType)

@classmethod
def mutate(cls, root, info, ids):
deleted_secrets = []

for id in ids:
secret = Secret.objects.get(id=id)
env = secret.environment
org = env.app.organisation

if not user_is_org_member(info.context.user.userId, org.id):
raise GraphQLError("You don't have permission to perform this action")

secret.updated_at = timezone.now()
secret.deleted_at = timezone.now()
secret.save()
deleted_secrets.append(secret)

ip_address, user_agent = get_resolver_request_meta(info.context)
org_member = OrganisationMember.objects.get(
user=info.context.user, organisation=org, deleted_at=None
)
log_secret_event(
secret, SecretEvent.DELETE, org_member, None, ip_address, user_agent
)

return BulkDeleteSecretMutation(secrets=deleted_secrets)


class ReadSecretMutation(graphene.Mutation):
class Arguments:
ids = graphene.List(graphene.ID)
Expand Down Expand Up @@ -810,7 +957,7 @@ 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
organisation=org, user=info.context.user, deleted_at=None
)

if not user_can_access_environment(info.context.user, secret.environment.id):
Expand All @@ -837,7 +984,7 @@ 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
organisation=org, user=info.context.user, deleted_at=None
)

if not user_can_access_environment(info.context.user, secret.environment.id):
Expand Down
7 changes: 7 additions & 0 deletions backend/backend/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
from .graphene.queries.quotas import resolve_organisation_plan
from .graphene.queries.license import resolve_license, resolve_organisation_license
from .graphene.mutations.environment import (
BulkCreateSecretMutation,
BulkDeleteSecretMutation,
BulkEditSecretMutation,
CreateEnvironmentKeyMutation,
CreateEnvironmentMutation,
CreateEnvironmentTokenMutation,
Expand Down Expand Up @@ -756,6 +759,10 @@ class Mutation(graphene.ObjectType):
delete_secret = DeleteSecretMutation.Field()
read_secret = ReadSecretMutation.Field()

create_secrets = BulkCreateSecretMutation.Field()
edit_secrets = BulkEditSecretMutation.Field()
delete_secrets = BulkDeleteSecretMutation.Field()

create_override = CreatePersonalSecretMutation.Field()
remove_override = DeletePersonalSecretMutation.Field()

Expand Down
5 changes: 5 additions & 0 deletions frontend/apollo/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const documents = {
"mutation CreateApplication($id: ID!, $organisationId: ID!, $name: String!, $identityKey: String!, $appToken: String!, $appSeed: String!, $wrappedKeyShare: String!, $appVersion: Int!) {\n createApp(\n id: $id\n organisationId: $organisationId\n name: $name\n identityKey: $identityKey\n appToken: $appToken\n appSeed: $appSeed\n wrappedKeyShare: $wrappedKeyShare\n appVersion: $appVersion\n ) {\n app {\n id\n name\n identityKey\n }\n }\n}": types.CreateApplicationDocument,
"mutation CreateOrg($id: ID!, $name: String!, $identityKey: String!, $wrappedKeyring: String!, $wrappedRecovery: String!) {\n createOrganisation(\n id: $id\n name: $name\n identityKey: $identityKey\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n ) {\n organisation {\n id\n name\n memberId\n }\n }\n}": types.CreateOrgDocument,
"mutation DeleteApplication($id: ID!) {\n deleteApp(id: $id) {\n ok\n }\n}": types.DeleteApplicationDocument,
"mutation BulkProcessSecrets($secretsToCreate: [SecretInput!]!, $secretsToUpdate: [SecretInput!]!, $secretsToDelete: [ID!]!) {\n createSecrets(secretsData: $secretsToCreate) {\n secrets {\n id\n key\n value\n createdAt\n }\n }\n editSecrets(secretsData: $secretsToUpdate) {\n secrets {\n id\n updatedAt\n }\n }\n deleteSecrets(ids: $secretsToDelete) {\n secrets {\n id\n }\n }\n}": types.BulkProcessSecretsDocument,
"mutation CreateEnv($envInput: EnvironmentInput!, $adminKeys: [EnvironmentKeyInput], $wrappedSeed: String, $wrappedSalt: String) {\n createEnvironment(\n environmentData: $envInput\n adminKeys: $adminKeys\n wrappedSeed: $wrappedSeed\n wrappedSalt: $wrappedSalt\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n}": types.CreateEnvDocument,
"mutation CreateEnvKey($envId: ID!, $userId: ID, $wrappedSeed: String!, $wrappedSalt: String!, $identityKey: String!) {\n createEnvironmentKey(\n envId: $envId\n userId: $userId\n wrappedSeed: $wrappedSeed\n wrappedSalt: $wrappedSalt\n identityKey: $identityKey\n ) {\n environmentKey {\n id\n createdAt\n }\n }\n}": types.CreateEnvKeyDocument,
"mutation CreateEnvToken($envId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!) {\n createEnvironmentToken(\n envId: $envId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n ) {\n environmentToken {\n id\n createdAt\n }\n }\n}": types.CreateEnvTokenDocument,
Expand Down Expand Up @@ -146,6 +147,10 @@ export function graphql(source: "mutation CreateOrg($id: ID!, $name: String!, $i
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "mutation DeleteApplication($id: ID!) {\n deleteApp(id: $id) {\n ok\n }\n}"): (typeof documents)["mutation DeleteApplication($id: ID!) {\n deleteApp(id: $id) {\n ok\n }\n}"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "mutation BulkProcessSecrets($secretsToCreate: [SecretInput!]!, $secretsToUpdate: [SecretInput!]!, $secretsToDelete: [ID!]!) {\n createSecrets(secretsData: $secretsToCreate) {\n secrets {\n id\n key\n value\n createdAt\n }\n }\n editSecrets(secretsData: $secretsToUpdate) {\n secrets {\n id\n updatedAt\n }\n }\n deleteSecrets(ids: $secretsToDelete) {\n secrets {\n id\n }\n }\n}"): (typeof documents)["mutation BulkProcessSecrets($secretsToCreate: [SecretInput!]!, $secretsToUpdate: [SecretInput!]!, $secretsToDelete: [ID!]!) {\n createSecrets(secretsData: $secretsToCreate) {\n secrets {\n id\n key\n value\n createdAt\n }\n }\n editSecrets(secretsData: $secretsToUpdate) {\n secrets {\n id\n updatedAt\n }\n }\n deleteSecrets(ids: $secretsToDelete) {\n secrets {\n id\n }\n }\n}"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading

0 comments on commit 2bc581e

Please sign in to comment.