Skip to content

Commit 25e5289

Browse files
committed
fix(frontend): give a real error when the viewer has no environment key
Eight call sites read the viewer's environment key as `data.environmentKeys[0]` and destructure it immediately. That list is a queryset filtered by environment and member, so an empty result is a normal outcome rather than an error state, and destructuring it throws "Cannot destructure property 'wrappedSeed' of 'undefined'" from inside an async callback. The user is told nothing, and at the sites that do not catch, the dialog just stops responding. An empty list is reachable because the resolver guards on `user_can_access_app` while filtering keys by environment and member: - CreateEnvironmentMutation provisions `admin_keys` only, so a non-admin member with app access holds no key for an environment created after they were added. - AddAppMemberMutation takes a client-supplied `env_keys` list, so a member can be granted app access for a subset of environments. Add `requireEnvironmentKey` next to the other environment crypto helpers and use it at each site, so the failure names the environment and says what to do about it. Behaviour is otherwise unchanged: the sites that threw still throw, with a message that means something.
1 parent 6a46708 commit 25e5289

9 files changed

Lines changed: 77 additions & 14 deletions

File tree

frontend/app/[team]/access/members/_components/RoleSelector.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import { toast } from 'react-toastify'
1111
import { PermissionPolicy, isRoleCryptoSafe, userHasGlobalAccess } from '@/utils/access/permissions'
1212
import { RoleLabel } from '@/components/users/RoleLabel'
1313
import { KeyringContext } from '@/contexts/keyringContext'
14-
import { unwrapEnvSecretsForUser, wrapEnvSecretsForAccount } from '@/utils/crypto'
14+
import {
15+
requireEnvironmentKey,
16+
unwrapEnvSecretsForUser,
17+
wrapEnvSecretsForAccount,
18+
} from '@/utils/crypto'
1519
import { userHasPermission } from '@/utils/access/permissions'
1620
import { updateServiceAccountHandlers } from '@/utils/crypto/service-accounts'
1721
import GetOrganisationMembers from '@/graphql/queries/organisation/getOrganisationMembers.gql'
@@ -101,7 +105,7 @@ export const RoleSelector = (props: {
101105
wrappedSeed: userWrappedSeed,
102106
wrappedSalt: userWrappedSalt,
103107
identityKey,
104-
} = data.environmentKeys[0]
108+
} = requireEnvironmentKey(data.environmentKeys, env.name)
105109

106110
// Unwrap env keys for current logged in user
107111
const { seed, salt } = await unwrapEnvSecretsForUser(

frontend/app/[team]/apps/[app]/access/members/_components/AddMemberDialog.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ import { toast } from 'react-toastify'
2626
import { Avatar } from '@/components/common/Avatar'
2727
import { Alert } from '@/components/common/Alert'
2828
import Link from 'next/link'
29-
import { unwrapEnvSecretsForUser, wrapEnvSecretsForAccount } from '@/utils/crypto'
29+
import {
30+
requireEnvironmentKey,
31+
unwrapEnvSecretsForUser,
32+
wrapEnvSecretsForAccount,
33+
} from '@/utils/crypto'
3034
import GenericDialog from '@/components/common/GenericDialog'
3135
import { organisationContext } from '@/contexts/organisationContext'
3236
import { useAppPermissions } from '@/hooks/useAppPermissions'
@@ -239,7 +243,7 @@ export const AddMemberDialog = ({ appId }: { appId: string }) => {
239243
wrappedSeed: userWrappedSeed,
240244
wrappedSalt: userWrappedSalt,
241245
identityKey,
242-
} = data.environmentKeys[0]
246+
} = requireEnvironmentKey(data.environmentKeys, env.name)
243247

244248
const { seed, salt } = await unwrapEnvSecretsForUser(
245249
userWrappedSeed,

frontend/app/[team]/apps/[app]/access/members/_components/ManageUserAccessDialog.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ import { userHasGlobalAccess } from '@/utils/access/permissions'
2121
import { useAppPermissions } from '@/hooks/useAppPermissions'
2222
import { Alert } from '@/components/common/Alert'
2323
import Link from 'next/link'
24-
import { arraysEqual, unwrapEnvSecretsForUser, wrapEnvSecretsForAccount } from '@/utils/crypto'
24+
import {
25+
arraysEqual,
26+
requireEnvironmentKey,
27+
unwrapEnvSecretsForUser,
28+
wrapEnvSecretsForAccount,
29+
} from '@/utils/crypto'
2530
import GenericDialog from '@/components/common/GenericDialog'
2631
import { sortEnvs } from '@/utils/secrets'
2732
import { useSearchParams } from 'next/navigation'
@@ -167,7 +172,7 @@ export const ManageUserAccessDialog = ({
167172
wrappedSeed: userWrappedSeed,
168173
wrappedSalt: userWrappedSalt,
169174
identityKey,
170-
} = data.environmentKeys[0]
175+
} = requireEnvironmentKey(data.environmentKeys, env.name)
171176

172177
const { seed, salt } = await unwrapEnvSecretsForUser(
173178
userWrappedSeed,

frontend/app/[team]/apps/[app]/access/service-accounts/_components/AddAccountDialog.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ import { KeyringContext } from '@/contexts/keyringContext'
2828
import { useAppPermissions } from '@/hooks/useAppPermissions'
2929
import { Alert } from '@/components/common/Alert'
3030
import Link from 'next/link'
31-
import { unwrapEnvSecretsForUser, wrapEnvSecretsForAccount } from '@/utils/crypto'
31+
import {
32+
requireEnvironmentKey,
33+
unwrapEnvSecretsForUser,
34+
wrapEnvSecretsForAccount,
35+
} from '@/utils/crypto'
3236
import { useSearchParams, useParams } from 'next/navigation'
3337
import GenericDialog from '@/components/common/GenericDialog'
3438
import { EmptyState } from '@/components/common/EmptyState'
@@ -242,7 +246,7 @@ export const AddAccountDialog = ({ appId }: { appId: string }) => {
242246
wrappedSeed: userWrappedSeed,
243247
wrappedSalt: userWrappedSalt,
244248
identityKey,
245-
} = data.environmentKeys[0]
249+
} = requireEnvironmentKey(data.environmentKeys, env.name)
246250

247251
const { seed, salt } = await unwrapEnvSecretsForUser(
248252
userWrappedSeed,

frontend/app/[team]/apps/[app]/access/service-accounts/_components/ManageAccountAccessDialog.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ import { userHasGlobalAccess } from '@/utils/access/permissions'
2323
import { useAppPermissions } from '@/hooks/useAppPermissions'
2424
import { Alert } from '@/components/common/Alert'
2525
import Link from 'next/link'
26-
import { arraysEqual, unwrapEnvSecretsForUser, wrapEnvSecretsForAccount } from '@/utils/crypto'
26+
import {
27+
arraysEqual,
28+
requireEnvironmentKey,
29+
unwrapEnvSecretsForUser,
30+
wrapEnvSecretsForAccount,
31+
} from '@/utils/crypto'
2732
import GenericDialog from '@/components/common/GenericDialog'
2833
import { sortEnvs } from '@/utils/secrets'
2934
import { useSearchParams } from 'next/navigation'
@@ -171,7 +176,7 @@ export const ManageAccountAccessDialog = ({
171176
wrappedSeed: userWrappedSeed,
172177
wrappedSalt: userWrappedSalt,
173178
identityKey,
174-
} = data.environmentKeys[0]
179+
} = requireEnvironmentKey(data.environmentKeys, env.name)
175180

176181
const { seed, salt } = await unwrapEnvSecretsForUser(
177182
userWrappedSeed,

frontend/components/apps/EnableSSEDialog.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import GetAppSyncStatus from '@/graphql/queries/syncing/getAppSyncStatus.gql'
1515
import { GetAppDetail } from '@/graphql/queries/getAppDetail.gql'
1616
import { FaServer } from 'react-icons/fa6'
1717
import { organisationContext } from '@/contexts/organisationContext'
18-
import { unwrapEnvSecretsForUser, wrapEnvSecretsForServer } from '@/utils/crypto'
18+
import {
19+
requireEnvironmentKey,
20+
unwrapEnvSecretsForUser,
21+
wrapEnvSecretsForServer,
22+
} from '@/utils/crypto'
1923
import { useAppPermissions } from '@/hooks/useAppPermissions'
2024
import Link from 'next/link'
2125

@@ -66,7 +70,7 @@ export const EnableSSEDialog = (props: { appId: string }) => {
6670
wrappedSeed: userWrappedSeed,
6771
wrappedSalt: userWrappedSalt,
6872
identityKey,
69-
} = envKeyData.environmentKeys[0]
73+
} = requireEnvironmentKey(envKeyData.environmentKeys, env.name)
7074

7175
const { seed, salt } = await unwrapEnvSecretsForUser(
7276
userWrappedSeed,

frontend/components/apps/tokens/CreateServiceTokenDialog.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
newServiceTokenKeys,
2626
splitSecret,
2727
getWrappedKeyShare,
28+
requireEnvironmentKey,
2829
unwrapEnvSecretsForUser,
2930
wrapEnvSecretsForServiceToken,
3031
} from '@/utils/crypto'
@@ -123,7 +124,7 @@ export const CreateServiceTokenDialog = (props: { organisationId: string; appId:
123124
wrappedSeed: userWrappedSeed,
124125
wrappedSalt: userWrappedSalt,
125126
identityKey,
126-
} = data.environmentKeys[0]
127+
} = requireEnvironmentKey(data.environmentKeys, env.name)
127128

128129
const { seed, salt } = await unwrapEnvSecretsForUser(
129130
userWrappedSeed,

frontend/components/environments/secrets/HistoryDialog.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
getUserKxPrivateKey,
1717
decryptAsymmetric,
1818
envKeyring,
19+
requireEnvironmentKey,
1920
} from '@/utils/crypto'
2021
import { KeyringContext } from '@/contexts/keyringContext'
2122
import Spinner from '@/components/common/Spinner'
@@ -77,7 +78,10 @@ export const HistoryDialog = ({
7778
})
7879

7980
if (data && keyring) {
80-
const wrappedSeed = data.environmentKeys[0].wrappedSeed
81+
const wrappedSeed = requireEnvironmentKey(
82+
data.environmentKeys,
83+
secret.environment.name
84+
).wrappedSeed
8185

8286
const userKxKeys = {
8387
publicKey: await getUserKxPublicKey(keyring.publicKey),

frontend/utils/crypto/environments.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,38 @@ export const wrapEnvSecretsForServiceToken = async (
284284
}
285285
}
286286

287+
/**
288+
* Read the viewer's own environment key out of a `GetEnvironmentKey` result.
289+
*
290+
* `environmentKeys` resolves to a queryset filtered by member, so an empty list
291+
* is a normal outcome rather than an error state: it means the acting member
292+
* holds no key for this environment, e.g. they were given app access without
293+
* that environment, or the environment was created after their keys were
294+
* provisioned. Indexing `[0]` and destructuring it turns that case into
295+
* "Cannot destructure property 'wrappedSeed' of 'undefined'" thrown from inside
296+
* an async callback, which tells the user nothing and, at the call sites that
297+
* do not catch, leaves the dialog looking inert.
298+
*
299+
* @param {EnvironmentKeyType[] | null | undefined} environmentKeys - `data.environmentKeys` from the query.
300+
* @param {string} [envName] - Environment name, used to make the error actionable.
301+
* @returns {EnvironmentKeyType} - The viewer's environment key.
302+
* @throws {Error} - If the viewer holds no key for this environment.
303+
*/
304+
export const requireEnvironmentKey = (
305+
environmentKeys: EnvironmentKeyType[] | null | undefined,
306+
envName?: string
307+
): EnvironmentKeyType => {
308+
const environmentKey = environmentKeys?.[0]
309+
310+
if (!environmentKey) {
311+
throw new Error(
312+
`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.`
313+
)
314+
}
315+
316+
return environmentKey
317+
}
318+
287319
/**
288320
* Unwraps environment secrets for a user.
289321
*

0 commit comments

Comments
 (0)