diff --git a/backend/api/migrations/0089_alter_serviceaccounthandler_service_account.py b/backend/api/migrations/0089_alter_serviceaccounthandler_service_account.py new file mode 100644 index 00000000..c5b7e28d --- /dev/null +++ b/backend/api/migrations/0089_alter_serviceaccounthandler_service_account.py @@ -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'), + ), + ] diff --git a/backend/api/migrations/0090_alter_serviceaccount_organisation.py b/backend/api/migrations/0090_alter_serviceaccount_organisation.py new file mode 100644 index 00000000..6c0911ac --- /dev/null +++ b/backend/api/migrations/0090_alter_serviceaccount_organisation.py @@ -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'), + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index 0786d298..3a9e0f8a 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -218,7 +218,9 @@ def delete(self, *args, **kwargs): class ServiceAccount(models.Model): id = models.TextField(default=uuid4, primary_key=True, editable=False) name = models.CharField(max_length=255) - organisation = models.ForeignKey(Organisation, on_delete=models.CASCADE) + organisation = models.ForeignKey( + Organisation, on_delete=models.CASCADE, related_name="service_accounts" + ) role = models.ForeignKey( Role, on_delete=models.SET_NULL, @@ -236,7 +238,9 @@ class ServiceAccount(models.Model): class ServiceAccountHandler(models.Model): id = models.TextField(default=uuid4, primary_key=True) - service_account = models.ForeignKey(ServiceAccount, on_delete=models.CASCADE) + service_account = models.ForeignKey( + ServiceAccount, on_delete=models.CASCADE, related_name="handlers" + ) user = models.ForeignKey(OrganisationMember, on_delete=models.CASCADE) wrapped_keyring = models.TextField() wrapped_recovery = models.TextField() diff --git a/backend/backend/graphene/mutations/service_accounts.py b/backend/backend/graphene/mutations/service_accounts.py index 4be3ac96..4493949e 100644 --- a/backend/backend/graphene/mutations/service_accounts.py +++ b/backend/backend/graphene/mutations/service_accounts.py @@ -8,12 +8,13 @@ ServiceAccountHandler, ServiceAccountToken, ) -from api.utils.access.permissions import user_has_permission +from api.utils.access.permissions import user_has_permission, user_is_org_member from backend.graphene.types import ServiceAccountTokenType, ServiceAccountType from datetime import datetime class ServiceAccountHandlerInput(graphene.InputObjectType): + service_account_id = graphene.ID(required=False) member_id = graphene.ID(required=False) wrapped_keyring = graphene.String(required=True) wrapped_recovery = graphene.String(required=True) @@ -141,24 +142,27 @@ def mutate(cls, root, info, service_account_id, name, role_id): class UpdateServiceAccountHandlersMutation(graphene.Mutation): class Arguments: - service_account_id = graphene.ID() + organisation_id = graphene.ID() handlers = graphene.List(ServiceAccountHandlerInput) - service_account = graphene.Field(ServiceAccountType) + ok = graphene.Boolean() @classmethod - def mutate(cls, root, info, service_account_id, handlers): + def mutate(cls, root, info, organisation_id, handlers): user = info.context.user - service_account = ServiceAccount.objects.get(id=service_account_id) + org = Organisation.objects.get(id=organisation_id) - if not user_has_permission( - user, "update", "ServiceAccounts", service_account.organisation - ): + if not user_is_org_member(user.userId, organisation_id): raise GraphQLError( - "You don't have the permissions required to update Service Accounts in this organisation" + "You are not a member of this organisation and cannot perform this operation" ) + for account in org.service_accounts.all(): + [handler.delete() for handler in account.handlers.all()] + for handler in handlers: + service_account = ServiceAccount.objects.get(id=handler.service_account_id) + if not ServiceAccountHandler.objects.filter( service_account=service_account, user_id=handler.member_id ).exists(): @@ -169,9 +173,7 @@ def mutate(cls, root, info, service_account_id, handlers): wrapped_recovery=handler.wrapped_recovery, ) - return EnableServiceAccountThirdPartyAuthMutation( - service_account=service_account - ) + return UpdateServiceAccountHandlersMutation(ok=True) class DeleteServiceAccountMutation(graphene.Mutation): diff --git a/frontend/apollo/gql.ts b/frontend/apollo/gql.ts index 2f1c7b16..1a1bdda9 100644 --- a/frontend/apollo/gql.ts +++ b/frontend/apollo/gql.ts @@ -54,6 +54,7 @@ const documents = { "mutation CreateSAToken($serviceAccountId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $expiry: BigInt) {\n createServiceAccountToken(\n serviceAccountId: $serviceAccountId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n expiry: $expiry\n ) {\n token {\n id\n }\n }\n}": types.CreateSaTokenDocument, "mutation DeleteServiceAccountOp($id: ID!) {\n deleteServiceAccount(serviceAccountId: $id) {\n ok\n }\n}": types.DeleteServiceAccountOpDocument, "mutation DeleteServiceAccountTokenOp($id: ID!) {\n deleteServiceAccountToken(tokenId: $id) {\n ok\n }\n}": types.DeleteServiceAccountTokenOpDocument, + "mutation UpdateServiceAccountHandlerKeys($orgId: ID!, $handlers: [ServiceAccountHandlerInput]) {\n updateServiceAccountHandlers(organisationId: $orgId, handlers: $handlers) {\n ok\n }\n}": types.UpdateServiceAccountHandlerKeysDocument, "mutation CreateNewAWSSecretsSync($envId: ID!, $path: String!, $credentialId: ID!, $secretName: String!, $kmsId: String) {\n createAwsSecretSync(\n envId: $envId\n path: $path\n credentialId: $credentialId\n secretName: $secretName\n kmsId: $kmsId\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}": types.CreateNewAwsSecretsSyncDocument, "mutation CreateNewCfPagesSync($envId: ID!, $path: String!, $projectName: String!, $deploymentId: ID!, $projectEnv: String!, $credentialId: ID!) {\n createCloudflarePagesSync(\n envId: $envId\n path: $path\n projectName: $projectName\n deploymentId: $deploymentId\n projectEnv: $projectEnv\n credentialId: $credentialId\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}": types.CreateNewCfPagesSyncDocument, "mutation DeleteProviderCreds($credentialId: ID!) {\n deleteProviderCredentials(credentialId: $credentialId) {\n ok\n }\n}": types.DeleteProviderCredsDocument, @@ -294,6 +295,10 @@ export function graphql(source: "mutation DeleteServiceAccountOp($id: ID!) {\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 DeleteServiceAccountTokenOp($id: ID!) {\n deleteServiceAccountToken(tokenId: $id) {\n ok\n }\n}"): (typeof documents)["mutation DeleteServiceAccountTokenOp($id: ID!) {\n deleteServiceAccountToken(tokenId: $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 UpdateServiceAccountHandlerKeys($orgId: ID!, $handlers: [ServiceAccountHandlerInput]) {\n updateServiceAccountHandlers(organisationId: $orgId, handlers: $handlers) {\n ok\n }\n}"): (typeof documents)["mutation UpdateServiceAccountHandlerKeys($orgId: ID!, $handlers: [ServiceAccountHandlerInput]) {\n updateServiceAccountHandlers(organisationId: $orgId, handlers: $handlers) {\n ok\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/frontend/apollo/graphql.ts b/frontend/apollo/graphql.ts index 989fdd13..4777f5bc 100644 --- a/frontend/apollo/graphql.ts +++ b/frontend/apollo/graphql.ts @@ -1064,7 +1064,7 @@ export type MutationUpdateProviderCredentialsArgs = { export type MutationUpdateServiceAccountHandlersArgs = { handlers?: InputMaybe>>; - serviceAccountId?: InputMaybe; + organisationId?: InputMaybe; }; @@ -1595,6 +1595,7 @@ export type ServerEnvironmentKeyType = { export type ServiceAccountHandlerInput = { memberId?: InputMaybe; + serviceAccountId?: InputMaybe; wrappedKeyring: Scalars['String']['input']; wrappedRecovery: Scalars['String']['input']; }; @@ -1717,7 +1718,7 @@ export type UpdateProviderCredentials = { export type UpdateServiceAccountHandlersMutation = { __typename?: 'UpdateServiceAccountHandlersMutation'; - serviceAccount?: Maybe; + ok?: Maybe; }; export type UpdateSyncAuthentication = { @@ -2110,6 +2111,14 @@ export type DeleteServiceAccountTokenOpMutationVariables = Exact<{ export type DeleteServiceAccountTokenOpMutation = { __typename?: 'Mutation', deleteServiceAccountToken?: { __typename?: 'DeleteServiceAccountTokenMutation', ok?: boolean | null } | null }; +export type UpdateServiceAccountHandlerKeysMutationVariables = Exact<{ + orgId: Scalars['ID']['input']; + handlers?: InputMaybe> | InputMaybe>; +}>; + + +export type UpdateServiceAccountHandlerKeysMutation = { __typename?: 'Mutation', updateServiceAccountHandlers?: { __typename?: 'UpdateServiceAccountHandlersMutation', ok?: boolean | null } | null }; + export type CreateNewAwsSecretsSyncMutationVariables = Exact<{ envId: Scalars['ID']['input']; path: Scalars['String']['input']; @@ -2622,6 +2631,7 @@ export const CreateServiceAccountOpDocument = {"kind":"Document","definitions":[ export const CreateSaTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateSAToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"serviceAccountId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"expiry"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createServiceAccountToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"serviceAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"serviceAccountId"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"identityKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyShare"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}}},{"kind":"Argument","name":{"kind":"Name","value":"expiry"},"value":{"kind":"Variable","name":{"kind":"Name","value":"expiry"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const DeleteServiceAccountOpDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteServiceAccountOp"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteServiceAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"serviceAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; export const DeleteServiceAccountTokenOpDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteServiceAccountTokenOp"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteServiceAccountToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"tokenId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; +export const UpdateServiceAccountHandlerKeysDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateServiceAccountHandlerKeys"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"handlers"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServiceAccountHandlerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateServiceAccountHandlers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"handlers"},"value":{"kind":"Variable","name":{"kind":"Name","value":"handlers"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; export const CreateNewAwsSecretsSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewAWSSecretsSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"secretName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"kmsId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createAwsSecretSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}},{"kind":"Argument","name":{"kind":"Name","value":"secretName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"secretName"}}},{"kind":"Argument","name":{"kind":"Name","value":"kmsId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"kmsId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sync"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateNewCfPagesSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewCfPagesSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"deploymentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectEnv"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createCloudflarePagesSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"projectName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectName"}}},{"kind":"Argument","name":{"kind":"Name","value":"deploymentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"deploymentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"projectEnv"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectEnv"}}},{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sync"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const DeleteProviderCredsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteProviderCreds"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteProviderCredentials"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; diff --git a/frontend/apollo/schema.graphql b/frontend/apollo/schema.graphql index d5cc1c42..3a961f78 100644 --- a/frontend/apollo/schema.graphql +++ b/frontend/apollo/schema.graphql @@ -659,7 +659,7 @@ type Mutation { deleteCustomRole(id: ID!): DeleteCustomRoleMutation createServiceAccount(handlers: [ServiceAccountHandlerInput], identityKey: String, name: String, organisationId: ID, roleId: ID, serverWrappedKeyring: String, serverWrappedRecovery: String): CreateServiceAccountMutation enableServiceAccountThirdPartyAuth(serverWrappedKeyring: String, serverWrappedRecovery: String, serviceAccountId: ID): EnableServiceAccountThirdPartyAuthMutation - updateServiceAccountHandlers(handlers: [ServiceAccountHandlerInput], serviceAccountId: ID): UpdateServiceAccountHandlersMutation + updateServiceAccountHandlers(handlers: [ServiceAccountHandlerInput], organisationId: ID): UpdateServiceAccountHandlersMutation deleteServiceAccount(serviceAccountId: ID): DeleteServiceAccountMutation createServiceAccountToken(expiry: BigInt, identityKey: String!, name: String!, serviceAccountId: ID, token: String!, wrappedKeyShare: String!): CreateServiceAccountTokenMutation deleteServiceAccountToken(tokenId: ID): DeleteServiceAccountTokenMutation @@ -808,6 +808,7 @@ type CreateServiceAccountMutation { } input ServiceAccountHandlerInput { + serviceAccountId: ID memberId: ID wrappedKeyring: String! wrappedRecovery: String! @@ -818,7 +819,7 @@ type EnableServiceAccountThirdPartyAuthMutation { } type UpdateServiceAccountHandlersMutation { - serviceAccount: ServiceAccountType + ok: Boolean } type DeleteServiceAccountMutation { diff --git a/frontend/app/[team]/access/members/page.tsx b/frontend/app/[team]/access/members/page.tsx index 4a17bef9..d7639739 100644 --- a/frontend/app/[team]/access/members/page.tsx +++ b/frontend/app/[team]/access/members/page.tsx @@ -40,7 +40,7 @@ import clsx from 'clsx' import { copyToClipBoard } from '@/utils/clipboard' import { toast } from 'react-toastify' import { Avatar } from '@/components/common/Avatar' -import { userHasGlobalAccess, userIsAdmin } from '@/utils/access/permissions' +import { PermissionPolicy, userHasGlobalAccess, userIsAdmin } from '@/utils/access/permissions' import { RoleLabel } from '@/components/users/RoleLabel' import { KeyringContext } from '@/contexts/keyringContext' @@ -54,6 +54,7 @@ import { useSearchParams } from 'next/navigation' import { userHasPermission } from '@/utils/access/permissions' import { EmptyState } from '@/components/common/EmptyState' import Spinner from '@/components/common/Spinner' +import { updateServiceAccountHandlers } from '@/utils/crypto/service-accounts' const handleCopy = (val: string) => { copyToClipBoard(val) @@ -97,24 +98,28 @@ const RoleSelector = (props: { member: OrganisationMemberType }) => { /** * Handles the assignment of a user to a global access role. - * Env keys for all apps, are fetched and decrypted by the active user, then each key is re-encrypted for the new user and saved on the backend via the addMemberToApp mutation + * Env keys for all apps are fetched and decrypted by the active user, + * then each key is re-encrypted for the new user and saved on the backend via the addMemberToApp mutation. * - * @returns {void} + * @returns {Promise} */ - const assignGlobalAccess = () => { - if (appsData) { - const apps = appsData.apps + const assignGlobalAccess = async (): Promise => { + if (!appsData) { + return Promise.reject(new Error('No apps data available')) + } - // Function to process an individual app - const processApp = async (app: AppType) => { - // fetch envs for the app - const { data: appEnvsData } = await getAppEnvs({ variables: { appId: app.id } }) + const apps = appsData.apps + // Function to process an individual app + const processApp = async (app: AppType) => { + try { + // Fetch envs for the app + const { data: appEnvsData } = await getAppEnvs({ variables: { appId: app.id } }) const appEnvironments = appEnvsData.appEnvironments as EnvironmentType[] - // construct promises to encrypt each env key for the target user + // Construct promises to encrypt each env key for the target user const envKeyPromises = appEnvironments.map(async (env: EnvironmentType) => { - // fetch the current wrapped key for the environment + // Fetch the current wrapped key for the environment const { data } = await getEnvKey({ variables: { envId: env.id, @@ -128,20 +133,20 @@ const RoleSelector = (props: { member: OrganisationMemberType }) => { identityKey, } = data.environmentKeys[0] - // unwrap env keys for current logged in user + // Unwrap env keys for current logged in user const { seed, salt } = await unwrapEnvSecretsForUser( userWrappedSeed, userWrappedSalt, keyring! ) - // re-encrypt the env key for the target user + // Re-encrypt the env key for the target user const { wrappedSeed, wrappedSalt } = await wrapEnvSecretsForAccount( { seed, salt }, member ) - // resolve the promise with the mutation payload + // Return the mutation payload return { envId: env.id, userId: member.id, @@ -151,40 +156,43 @@ const RoleSelector = (props: { member: OrganisationMemberType }) => { } }) - // get mutation payloads with wrapped keys for each environment + // Get mutation payloads with wrapped keys for each environment const envKeyInputs = await Promise.all(envKeyPromises) - // add the user to this app, with wrapped keys for each environment + // Add the user to this app, with wrapped keys for each environment await addMemberToApp({ variables: { memberId: member.id, appId: app.id, envKeys: envKeyInputs }, }) + } catch (error) { + console.error(`Error processing app ${app.id}:`, error) + throw error // Propagate the error to be caught later } + } - // Process each app sequentially - const processAppsSequentially = async () => { - for (const app of apps) { - await processApp(app) - } + try { + // Process each app sequentially using for...of to ensure async operations complete in order + for (const app of apps) { + await processApp(app) } - // Call the function to process all apps sequentially - processAppsSequentially() - .then(async () => { - // All apps have been processed - const adminRole = roleOptions.find( - (option: RoleType) => option.name?.toLowerCase() === 'admin' - ) - await updateRole({ - variables: { - memberId: member.id, - roleId: adminRole.id, - }, - }) - toast.success('Updated member role', { autoClose: 2000 }) - }) - .catch((error) => { - console.error('Error processing apps:', error) + // After all apps have been processed, assign the global admin role + const adminRole = roleOptions.find( + (option: RoleType) => option.name?.toLowerCase() === 'admin' + ) + + if (adminRole) { + await updateRole({ + variables: { + memberId: member.id, + roleId: adminRole.id, + }, }) + } else { + throw new Error('Admin role not found') + } + } catch (error) { + console.error('Error assigning global access:', error) + throw error // Ensure the promise rejects if any error occurs } } @@ -192,6 +200,9 @@ const RoleSelector = (props: { member: OrganisationMemberType }) => { const newRoleHasGlobalAccess = userHasGlobalAccess(newRole.permissions) const currentUserHasGlobalAccess = userHasGlobalAccess(organisation?.role?.permissions) + const newRolePolicy: PermissionPolicy = JSON.parse(newRole.permissions) + const newRoleHasServiceAccountAccess = newRolePolicy.permissions['ServiceAccounts'].length > 0 + if (newRoleHasGlobalAccess && !currentUserHasGlobalAccess) { toast.error('You cannot assign users to this role as it requires global access!', { autoClose: 5000, @@ -209,16 +220,31 @@ const RoleSelector = (props: { member: OrganisationMemberType }) => { setRole(newRole) - if (newRoleHasGlobalAccess) assignGlobalAccess() - else { - await updateRole({ - variables: { - memberId: member.id, - roleId: newRole.id, - }, + const processUpdate = async () => { + return new Promise(async (resolve, reject) => { + try { + if (newRoleHasGlobalAccess) await assignGlobalAccess() + else { + await updateRole({ + variables: { + memberId: member.id, + roleId: newRole.id, + }, + }) + } + //if (newRoleHasServiceAccountAccess) + await updateServiceAccountHandlers(organisation!.id, keyring!) + resolve(true) + } catch (error) { + reject(error) + } }) - toast.success('Updated member role', { autoClose: 2000 }) } + await toast.promise(processUpdate, { + pending: 'Updating role...', + success: 'Updated role!', + error: 'Something went wrong!', + }) } const roleOptions = roleData?.roles.filter((option: RoleType) => option.name !== 'Owner') || [] diff --git a/frontend/app/[team]/access/service-accounts/[account]/_components/CreateServiceAccountTokenDialog.tsx b/frontend/app/[team]/access/service-accounts/[account]/_components/CreateServiceAccountTokenDialog.tsx index c53c6b57..a901da3f 100644 --- a/frontend/app/[team]/access/service-accounts/[account]/_components/CreateServiceAccountTokenDialog.tsx +++ b/frontend/app/[team]/access/service-accounts/[account]/_components/CreateServiceAccountTokenDialog.tsx @@ -7,7 +7,7 @@ import { humanReadableExpiry, tokenExpiryOptions, } from '@/utils/tokens' -import { Fragment, useContext, useRef, useState } from 'react' +import { Fragment, useContext, useEffect, useRef, useState } from 'react' import { FaCheckCircle, FaCircle, FaExternalLinkSquareAlt, FaPlus } from 'react-icons/fa' import { GetServiceAccounts } from '@/graphql/queries/service-accounts/getServiceAccounts.gql' import { CreateSAToken } from '@/graphql/mutations/service-accounts/createServiceAccountToken.gql' @@ -21,7 +21,7 @@ import { import { useMutation } from '@apollo/client' import { toast } from 'react-toastify' import { KeyringContext } from '@/contexts/keyringContext' -import { generateSAToken } from '@/utils/crypto/service-accounts' +import { generateSAToken, updateServiceAccountHandlers } from '@/utils/crypto/service-accounts' import { Alert } from '@/components/common/Alert' import CopyButton from '@/components/common/CopyButton' import { CliCommand } from '@/components/dashboard/CliCommand' @@ -29,6 +29,7 @@ import { getApiHost } from '@/utils/appConfig' import { Tab, RadioGroup } from '@headlessui/react' import clsx from 'clsx' import Link from 'next/link' +import { log } from 'console' export const CreateServiceAccountTokenDialog = ({ serviceAccount, @@ -38,6 +39,12 @@ export const CreateServiceAccountTokenDialog = ({ const { activeOrganisation: organisation } = useContext(organisationContext) const { keyring } = useContext(KeyringContext) + useEffect(() => { + if (organisation && keyring) { + updateServiceAccountHandlers(organisation.id, keyring) + } + }, [organisation, keyring]) + const serviceAccountHandler = serviceAccount.handlers?.find( (handler) => handler?.user.self === true ) diff --git a/frontend/app/[team]/access/service-accounts/_components/CreateServiceAccountDialog.tsx b/frontend/app/[team]/access/service-accounts/_components/CreateServiceAccountDialog.tsx index 0f2b9be0..66bed738 100644 --- a/frontend/app/[team]/access/service-accounts/_components/CreateServiceAccountDialog.tsx +++ b/frontend/app/[team]/access/service-accounts/_components/CreateServiceAccountDialog.tsx @@ -1,6 +1,6 @@ import { OrganisationMemberType, RoleType } from '@/apollo/graphql' import GenericDialog from '@/components/common/GenericDialog' -import { Fragment, useContext, useRef, useState } from 'react' +import { Fragment, useContext, useEffect, useRef, useState } from 'react' import { FaChevronDown, FaPlus } from 'react-icons/fa' import { GetServiceAccounts } from '@/graphql/queries/service-accounts/getServiceAccounts.gql' import { GetServiceAccountHandlers } from '@/graphql/queries/service-accounts/getServiceAccountHandlers.gql' @@ -21,7 +21,6 @@ import { Listbox } from '@headlessui/react' import clsx from 'clsx' import { ToggleSwitch } from '@/components/common/ToggleSwitch' import { Button } from '@/components/common/Button' -import { identity } from 'lodash' import { toast } from 'react-toastify' const bip39 = require('bip39') @@ -179,12 +178,12 @@ export const CreateServiceAccountDialog = () => { )} -
+ {/*
setThirdParty(!thirdParty)} /> -
+
*/}