feat: add namespace access validation for sealed secret routes - #1069
feat: add namespace access validation for sealed secret routes#1069CasLubbers wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds defense-in-depth authorization for sealed secret endpoints by tightening how the authz “subject” (team/namespace) is derived and by enforcing namespace membership checks on namespace-scoped sealed secret routes.
Changes:
- Tighten
authorize()teamId extraction so query/body teamId is only considered when declared by the OpenAPI operation. - Add
assertNamespaceAccess()and apply it to namespace-scoped sealed secret v2 routes. - Add server-side filtering on the cross-team
/v2/sealedsecretsendpoint to avoid leaking other teams’ secrets to non-platform admins.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/otomi-models.ts | Extends OpenAPI request typing to include resolved parameters/requestBody shape used by authz. |
| src/middleware/authz.ts | Restricts teamId sourcing from query/body to OpenAPI-declared fields to reduce injection surface. |
| src/api/v2/sealedsecrets.ts | Adds non-admin filtering for cross-team sealed secret collection responses. |
| src/api/v2/namespaces/{namespace}/sealedsecrets/{sealedSecretName}.ts | Enforces namespace membership checks for namespace-scoped sealed secret CRUD operations. |
| src/api/v2/namespaces/{namespace}/sealedsecrets.ts | Enforces namespace membership checks for namespace-scoped sealed secret list/create operations. |
| src/api/namespace-access.ts | Introduces centralized namespace access assertion helper. |
| package-lock.json | Lockfile metadata updates (peer flags) as part of dependency state. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| : all.filter((secret) => { | ||
| const teamId = | ||
| secret.metadata.namespace?.replace(/^team-/, '') ?? (secret.metadata.labels?.['apl.io/teamId'] as string) | ||
| return teamId && req.user.teams.includes(teamId) | ||
| }) |
| // Defense in depth: even if the ACL model is misconfigured, never hand a non-platformAdmin | ||
| // caller another team's secrets from this cross-team collection endpoint. | ||
| const v = req.user.isPlatformAdmin | ||
| ? all | ||
| : all.filter((secret) => { | ||
| const teamId = | ||
| secret.metadata.namespace?.replace(/^team-/, '') ?? (secret.metadata.labels?.['apl.io/teamId'] as string) | ||
| return teamId && req.user.teams.includes(teamId) | ||
| }) |
| export const getAplNamespaceSealedSecrets = async (req: OpenApiRequestExt, res: Response): Promise<void> => { | ||
| const { namespace } = req.params | ||
| assertNamespaceAccess(namespace, req.user) | ||
| debug(`getAplNamespaceSealedSecrets(${namespace}, ...)`) | ||
| const v = await req.otomi.getAplNamespaceSealedSecrets(namespace) |
| const schema = req.openapi?.schema | ||
|
|
||
| // Only trust query/body input for the authz subject when this specific operation's | ||
| // OpenAPI schema declares that field. | ||
| const queryTeamIdDeclared = schema?.parameters?.some((p) => p.name === 'teamId' && p.in === 'query') ?? false | ||
| const bodyTeamIdDeclared = !!schema?.requestBody?.content?.['application/json']?.schema?.properties?.teamId | ||
|
|
||
| // express-openapi-validator stores path params in req.openapi.pathParams | ||
| const teamId = req.openapi?.pathParams?.teamId ?? req.params?.teamId ?? req.query?.teamId ?? body?.teamId | ||
| const teamId = | ||
| req.openapi?.pathParams?.teamId ?? | ||
| req.params?.teamId ?? | ||
| (queryTeamIdDeclared ? req.query?.teamId : undefined) ?? | ||
| (bodyTeamIdDeclared ? body?.teamId : undefined) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/api/v2/namespaces/{namespace}/sealedsecrets/{sealedSecretName}.ts:44
- The debug log message is missing a closing ")" after the sealedSecretName interpolation, which makes logs harder to read/grep consistently.
debug(`editSealedSecret(${sealedSecretName} for namespace(${namespace}), patch)`)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/api/v2/namespaces/{namespace}/sealedsecrets/{sealedSecretName}.ts:44
- The debug message in the PATCH handler is missing a closing ")" after the sealed secret name, which makes logs harder to parse/search.
assertNamespaceAccess(namespace, req.user)
debug(`editSealedSecret(${sealedSecretName} for namespace(${namespace}), patch)`)
No description provided.