diff --git a/frontend/app/[team]/access/members/_components/RoleSelector.tsx b/frontend/app/[team]/access/members/_components/RoleSelector.tsx index d8df5ad19..202f662ee 100644 --- a/frontend/app/[team]/access/members/_components/RoleSelector.tsx +++ b/frontend/app/[team]/access/members/_components/RoleSelector.tsx @@ -11,7 +11,11 @@ import { toast } from 'react-toastify' import { PermissionPolicy, isRoleCryptoSafe, userHasGlobalAccess } from '@/utils/access/permissions' import { RoleLabel } from '@/components/users/RoleLabel' import { KeyringContext } from '@/contexts/keyringContext' -import { unwrapEnvSecretsForUser, wrapEnvSecretsForAccount } from '@/utils/crypto' +import { + requireEnvironmentKey, + unwrapEnvSecretsForUser, + wrapEnvSecretsForAccount, +} from '@/utils/crypto' import { userHasPermission } from '@/utils/access/permissions' import { updateServiceAccountHandlers } from '@/utils/crypto/service-accounts' import GetOrganisationMembers from '@/graphql/queries/organisation/getOrganisationMembers.gql' @@ -101,7 +105,7 @@ export const RoleSelector = (props: { wrappedSeed: userWrappedSeed, wrappedSalt: userWrappedSalt, identityKey, - } = data.environmentKeys[0] + } = requireEnvironmentKey(data.environmentKeys, env.name) // Unwrap env keys for current logged in user const { seed, salt } = await unwrapEnvSecretsForUser( diff --git a/frontend/app/[team]/apps/[app]/access/members/_components/AddMemberDialog.tsx b/frontend/app/[team]/apps/[app]/access/members/_components/AddMemberDialog.tsx index a8714f506..4b3ec65dc 100644 --- a/frontend/app/[team]/apps/[app]/access/members/_components/AddMemberDialog.tsx +++ b/frontend/app/[team]/apps/[app]/access/members/_components/AddMemberDialog.tsx @@ -26,7 +26,11 @@ import { toast } from 'react-toastify' import { Avatar } from '@/components/common/Avatar' import { Alert } from '@/components/common/Alert' import Link from 'next/link' -import { unwrapEnvSecretsForUser, wrapEnvSecretsForAccount } from '@/utils/crypto' +import { + requireEnvironmentKey, + unwrapEnvSecretsForUser, + wrapEnvSecretsForAccount, +} from '@/utils/crypto' import GenericDialog from '@/components/common/GenericDialog' import { organisationContext } from '@/contexts/organisationContext' import { useAppPermissions } from '@/hooks/useAppPermissions' @@ -239,7 +243,7 @@ export const AddMemberDialog = ({ appId }: { appId: string }) => { wrappedSeed: userWrappedSeed, wrappedSalt: userWrappedSalt, identityKey, - } = data.environmentKeys[0] + } = requireEnvironmentKey(data.environmentKeys, env.name) const { seed, salt } = await unwrapEnvSecretsForUser( userWrappedSeed, diff --git a/frontend/app/[team]/apps/[app]/access/members/_components/ManageUserAccessDialog.tsx b/frontend/app/[team]/apps/[app]/access/members/_components/ManageUserAccessDialog.tsx index 834c4c833..8417ec6f9 100644 --- a/frontend/app/[team]/apps/[app]/access/members/_components/ManageUserAccessDialog.tsx +++ b/frontend/app/[team]/apps/[app]/access/members/_components/ManageUserAccessDialog.tsx @@ -21,7 +21,12 @@ import { userHasGlobalAccess } from '@/utils/access/permissions' import { useAppPermissions } from '@/hooks/useAppPermissions' import { Alert } from '@/components/common/Alert' import Link from 'next/link' -import { arraysEqual, unwrapEnvSecretsForUser, wrapEnvSecretsForAccount } from '@/utils/crypto' +import { + arraysEqual, + requireEnvironmentKey, + unwrapEnvSecretsForUser, + wrapEnvSecretsForAccount, +} from '@/utils/crypto' import GenericDialog from '@/components/common/GenericDialog' import { sortEnvs } from '@/utils/secrets' import { useSearchParams } from 'next/navigation' @@ -167,7 +172,7 @@ export const ManageUserAccessDialog = ({ wrappedSeed: userWrappedSeed, wrappedSalt: userWrappedSalt, identityKey, - } = data.environmentKeys[0] + } = requireEnvironmentKey(data.environmentKeys, env.name) const { seed, salt } = await unwrapEnvSecretsForUser( userWrappedSeed, diff --git a/frontend/app/[team]/apps/[app]/access/service-accounts/_components/AddAccountDialog.tsx b/frontend/app/[team]/apps/[app]/access/service-accounts/_components/AddAccountDialog.tsx index 3da658339..fcb9ebe51 100644 --- a/frontend/app/[team]/apps/[app]/access/service-accounts/_components/AddAccountDialog.tsx +++ b/frontend/app/[team]/apps/[app]/access/service-accounts/_components/AddAccountDialog.tsx @@ -28,7 +28,11 @@ import { KeyringContext } from '@/contexts/keyringContext' import { useAppPermissions } from '@/hooks/useAppPermissions' import { Alert } from '@/components/common/Alert' import Link from 'next/link' -import { unwrapEnvSecretsForUser, wrapEnvSecretsForAccount } from '@/utils/crypto' +import { + requireEnvironmentKey, + unwrapEnvSecretsForUser, + wrapEnvSecretsForAccount, +} from '@/utils/crypto' import { useSearchParams, useParams } from 'next/navigation' import GenericDialog from '@/components/common/GenericDialog' import { EmptyState } from '@/components/common/EmptyState' @@ -242,7 +246,7 @@ export const AddAccountDialog = ({ appId }: { appId: string }) => { wrappedSeed: userWrappedSeed, wrappedSalt: userWrappedSalt, identityKey, - } = data.environmentKeys[0] + } = requireEnvironmentKey(data.environmentKeys, env.name) const { seed, salt } = await unwrapEnvSecretsForUser( userWrappedSeed, diff --git a/frontend/app/[team]/apps/[app]/access/service-accounts/_components/ManageAccountAccessDialog.tsx b/frontend/app/[team]/apps/[app]/access/service-accounts/_components/ManageAccountAccessDialog.tsx index 04659ce63..82d9dbf97 100644 --- a/frontend/app/[team]/apps/[app]/access/service-accounts/_components/ManageAccountAccessDialog.tsx +++ b/frontend/app/[team]/apps/[app]/access/service-accounts/_components/ManageAccountAccessDialog.tsx @@ -23,7 +23,12 @@ import { userHasGlobalAccess } from '@/utils/access/permissions' import { useAppPermissions } from '@/hooks/useAppPermissions' import { Alert } from '@/components/common/Alert' import Link from 'next/link' -import { arraysEqual, unwrapEnvSecretsForUser, wrapEnvSecretsForAccount } from '@/utils/crypto' +import { + arraysEqual, + requireEnvironmentKey, + unwrapEnvSecretsForUser, + wrapEnvSecretsForAccount, +} from '@/utils/crypto' import GenericDialog from '@/components/common/GenericDialog' import { sortEnvs } from '@/utils/secrets' import { useSearchParams } from 'next/navigation' @@ -171,7 +176,7 @@ export const ManageAccountAccessDialog = ({ wrappedSeed: userWrappedSeed, wrappedSalt: userWrappedSalt, identityKey, - } = data.environmentKeys[0] + } = requireEnvironmentKey(data.environmentKeys, env.name) const { seed, salt } = await unwrapEnvSecretsForUser( userWrappedSeed, diff --git a/frontend/components/apps/EnableSSEDialog.tsx b/frontend/components/apps/EnableSSEDialog.tsx index 8f34fd288..c3406e1c2 100644 --- a/frontend/components/apps/EnableSSEDialog.tsx +++ b/frontend/components/apps/EnableSSEDialog.tsx @@ -15,7 +15,11 @@ import GetAppSyncStatus from '@/graphql/queries/syncing/getAppSyncStatus.gql' import { GetAppDetail } from '@/graphql/queries/getAppDetail.gql' import { FaServer } from 'react-icons/fa6' import { organisationContext } from '@/contexts/organisationContext' -import { unwrapEnvSecretsForUser, wrapEnvSecretsForServer } from '@/utils/crypto' +import { + requireEnvironmentKey, + unwrapEnvSecretsForUser, + wrapEnvSecretsForServer, +} from '@/utils/crypto' import { useAppPermissions } from '@/hooks/useAppPermissions' import Link from 'next/link' @@ -66,7 +70,7 @@ export const EnableSSEDialog = (props: { appId: string }) => { wrappedSeed: userWrappedSeed, wrappedSalt: userWrappedSalt, identityKey, - } = envKeyData.environmentKeys[0] + } = requireEnvironmentKey(envKeyData.environmentKeys, env.name) const { seed, salt } = await unwrapEnvSecretsForUser( userWrappedSeed, diff --git a/frontend/components/apps/tokens/CreateServiceTokenDialog.tsx b/frontend/components/apps/tokens/CreateServiceTokenDialog.tsx index 0807aaef2..9a37c1e1c 100644 --- a/frontend/components/apps/tokens/CreateServiceTokenDialog.tsx +++ b/frontend/components/apps/tokens/CreateServiceTokenDialog.tsx @@ -25,6 +25,7 @@ import { newServiceTokenKeys, splitSecret, getWrappedKeyShare, + requireEnvironmentKey, unwrapEnvSecretsForUser, wrapEnvSecretsForServiceToken, } from '@/utils/crypto' @@ -123,7 +124,7 @@ export const CreateServiceTokenDialog = (props: { organisationId: string; appId: wrappedSeed: userWrappedSeed, wrappedSalt: userWrappedSalt, identityKey, - } = data.environmentKeys[0] + } = requireEnvironmentKey(data.environmentKeys, env.name) const { seed, salt } = await unwrapEnvSecretsForUser( userWrappedSeed, diff --git a/frontend/components/environments/secrets/HistoryDialog.tsx b/frontend/components/environments/secrets/HistoryDialog.tsx index 9d14c4213..efd846b0b 100644 --- a/frontend/components/environments/secrets/HistoryDialog.tsx +++ b/frontend/components/environments/secrets/HistoryDialog.tsx @@ -16,6 +16,7 @@ import { getUserKxPrivateKey, decryptAsymmetric, envKeyring, + requireEnvironmentKey, } from '@/utils/crypto' import { KeyringContext } from '@/contexts/keyringContext' import Spinner from '@/components/common/Spinner' @@ -77,7 +78,10 @@ export const HistoryDialog = ({ }) if (data && keyring) { - const wrappedSeed = data.environmentKeys[0].wrappedSeed + const wrappedSeed = requireEnvironmentKey( + data.environmentKeys, + secret.environment.name + ).wrappedSeed const userKxKeys = { publicKey: await getUserKxPublicKey(keyring.publicKey), diff --git a/frontend/utils/crypto/environments.ts b/frontend/utils/crypto/environments.ts index 79c3839b4..09b5e991d 100644 --- a/frontend/utils/crypto/environments.ts +++ b/frontend/utils/crypto/environments.ts @@ -284,6 +284,38 @@ export const wrapEnvSecretsForServiceToken = async ( } } +/** + * Read the viewer's own environment key out of a `GetEnvironmentKey` result. + * + * `environmentKeys` resolves to a queryset filtered by member, so an empty list + * is a normal outcome rather than an error state: it means the acting member + * holds no key for this environment, e.g. they were given app access without + * that environment, or the environment was created after their keys were + * provisioned. Indexing `[0]` and destructuring it turns that case into + * "Cannot destructure property 'wrappedSeed' of 'undefined'" thrown from inside + * an async callback, which tells the user nothing and, at the call sites that + * do not catch, leaves the dialog looking inert. + * + * @param {EnvironmentKeyType[] | null | undefined} environmentKeys - `data.environmentKeys` from the query. + * @param {string} [envName] - Environment name, used to make the error actionable. + * @returns {EnvironmentKeyType} - The viewer's environment key. + * @throws {Error} - If the viewer holds no key for this environment. + */ +export const requireEnvironmentKey = ( + environmentKeys: EnvironmentKeyType[] | null | undefined, + envName?: string +): EnvironmentKeyType => { + const environmentKey = environmentKeys?.[0] + + if (!environmentKey) { + throw new Error( + `You don't have a key for the ${envName ?? 'selected'} environment, so it can't be shared. Ask an admin to grant you access to it and try again.` + ) + } + + return environmentKey +} + /** * Unwraps environment secrets for a user. *