{"record":{"id":"5d9ece26c6055f27","repo":"immich-app/immich","slug":"missing-required-permission-requestedpermission","errorCode":null,"errorMessage":"Missing required permission: ${requestedPermission}","messagePattern":"Missing required permission: (.+?)","errorType":"http","errorClass":"ForbiddenException","httpStatus":403,"severity":"error","filePath":"server/src/services/auth.service.ts","lineNumber":231,"sourceCode":"    const { adminRoute, sharedLinkRoute, uri } = metadata;\n    const requestedPermission = metadata.permission ?? Permission.All;\n\n    if (!authDto.user.isAdmin && adminRoute) {\n      this.logger.warn(`Denied access to admin only route: ${uri}`);\n      throw new ForbiddenException('Forbidden');\n    }\n\n    if (authDto.sharedLink && !sharedLinkRoute) {\n      this.logger.warn(`Denied access to non-shared route: ${uri}`);\n      throw new ForbiddenException('Forbidden');\n    }\n\n    if (\n      authDto.apiKey &&\n      requestedPermission !== false &&\n      !isGranted({ requested: [requestedPermission], current: authDto.apiKey.permissions })\n    ) {\n      throw new ForbiddenException(`Missing required permission: ${requestedPermission}`);\n    }\n\n    return authDto;\n  }\n\n  private async validate({ headers, queryParams }: Omit<ValidateRequest, 'metadata'>): Promise<AuthDto> {\n    const shareKey = (headers[ImmichHeader.SharedLinkKey] || queryParams[ImmichQuery.SharedLinkKey]) as string;\n    const shareSlug = (headers[ImmichHeader.SharedLinkSlug] || queryParams[ImmichQuery.SharedLinkSlug]) as string;\n    const session = (headers[ImmichHeader.UserToken] ||\n      headers[ImmichHeader.SessionToken] ||\n      queryParams[ImmichQuery.SessionKey] ||\n      this.getBearerToken(headers) ||\n      this.getCookieToken(headers)) as string;\n    const apiKey = (headers[ImmichHeader.ApiKey] || queryParams[ImmichQuery.ApiKey]) as string;\n\n    if (shareKey) {\n      return this.validateSharedLinkKey(shareKey);\n    }","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/auth.service.ts#L213-L249","documentation":"ForbiddenException (HTTP 403) thrown by AuthService.authenticate when the request carries an API key and the route's requested permission (defaults to Permission.All when metadata.permission is unset) is not granted by the key's permissions via isGranted. The message interpolates the missing permission name to aid debugging.","triggerScenarios":"Any authenticated request made with an x-api-key whose permission set does not include the route's required Permission. Example: an API key scoped to asset.read hitting POST /assets (asset.create), or any key on a route with no explicit permission (defaults to All).","commonSituations":"API key created with limited scopes and reused for an admin/general call; route added or upgraded to require a permission the key lacks; client hard-codes a stale API key after permissions were tightened.","solutions":["Issue a new API key whose permissions include the value named in the error message.","If the route genuinely needs full access, use a session token instead of an API key.","Have an admin edit the key's permissions in /api-keys settings.","Audit isGranted outputs against the current Permission enum after upgrades."],"exampleFix":"// before\nawait axios.post('/assets', data, { headers: { 'x-api-key': readOnlyKey } });\n// -> 403 Missing required permission: asset.create\n\n// after\nconst key = await axios.post('/api-keys', { name: 'uploader', permissions: ['asset.create'] }, { headers: { Authorization: `Bearer ${adminSession}` } });\nawait axios.post('/assets', data, { headers: { 'x-api-key': key.data.secret } });","handlingStrategy":"validation","validationCode":"async function keyHasPermission(key: string, perm: string): Promise<boolean> {\n  const { data } = await axios.get('/api-keys', { headers: { 'x-api-key': key } });\n  return data.permissions?.includes(perm);\n}","typeGuard":"function keyGrants(perms: string[], required: string): boolean {\n  return perms.includes(required) || perms.includes('all');\n}","tryCatchPattern":"try {\n  await axios.post('/assets', data, { headers: { 'x-api-key': key } });\n} catch (e) {\n  if (e.response?.status === 403 && /Missing required permission/.test(e.response?.data?.message || '')) {\n    const required = e.response.data.message.split(': ').pop();\n    await provisionKeyWithPermission(required);\n  } else throw e;\n}","preventionTips":["Scope API keys to exactly the permissions they need; reissue when routes change.","After upgrades, audit key permissions against the current Permission enum.","Prefer session tokens for general/admin actions."],"tags":["auth","api-key","authorization","nestjs","immich"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}