Skip to content

Commit 0ab86d1

Browse files
committed
fix: cleanup middleware authz
1 parent 69ac22d commit 0ab86d1

File tree

4 files changed

+44
-93
lines changed

4 files changed

+44
-93
lines changed

src/api/apiDocs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Debug from 'debug'
22
import { Response } from 'express'
33
import { OpenApiRequest } from 'src/otomi-models'
4+
import { getSpec } from '../app'
45

56
const debug = Debug('otomi:api')
67

@@ -10,5 +11,5 @@ const debug = Debug('otomi:api')
1011
*/
1112
export const getApiDoc = (req: OpenApiRequest, res: Response): void => {
1213
debug('apiDocs')
13-
res.json(req.apiDoc)
14+
res.json(getSpec().spec)
1415
}

src/api/v1/apiDocs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Debug from 'debug'
22
import { Response } from 'express'
3+
import { getSpec } from '../../app'
34
import { OpenApiRequest } from 'src/otomi-models'
45

56
const debug = Debug('otomi:v1:api')
@@ -10,5 +11,5 @@ const debug = Debug('otomi:v1:api')
1011
*/
1112
export const getApiDoc = (req: OpenApiRequest, res: Response): void => {
1213
debug('apiDocs')
13-
res.json(req.apiDoc)
14+
res.json(getSpec().spec)
1415
}

src/middleware/authz.ts

Lines changed: 29 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
/* eslint-disable no-param-reassign */
22
import { getSpec } from 'src/app'
33
import { debug } from 'console'
4-
import { RequestHandler } from 'express'
54
import { find } from 'lodash'
65
import get from 'lodash/get'
7-
import Authz, { getTeamSelfServiceAuthz } from 'src/authz'
6+
import Authz from 'src/authz'
87
import { HttpError } from 'src/error'
98
import { OpenApiRequestExt } from 'src/otomi-models'
10-
import OtomiStack from 'src/otomi-stack'
119
import { RepoService } from '../services/RepoService'
12-
import { getSessionStack } from './session'
1310

1411
const HttpMethodMapping: Record<string, string> = {
1512
DELETE: 'delete',
@@ -41,34 +38,29 @@ function renameKeys(obj: Record<string, any>) {
4138
// }
4239
// }
4340

44-
export function authorize(req: OpenApiRequestExt, res, next, authz: Authz, repoService: RepoService): RequestHandler {
45-
const { params, query, body, user } = req
41+
/**
42+
* Authorize a request based on RBAC and ABAC rules.
43+
* Called by the security handler.
44+
* Throws HttpError if authorization fails.
45+
*/
46+
export function authorize(req: OpenApiRequestExt, authz: Authz, repoService: RepoService): void {
47+
const { body, user } = req
4648
// express-openapi-validator stores path params in req.openapi.pathParams
47-
const teamId = req.openapi?.pathParams?.teamId ?? params?.teamId ?? query?.teamId ?? body?.teamId
49+
const teamId = req.openapi?.pathParams?.teamId ?? req.params?.teamId ?? req.query?.teamId ?? body?.teamId
4850
const action = HttpMethodMapping[req.method]
4951

50-
// express-openapi-validator doesn't expose custom x- extensions on req.openapi.schema
51-
// so we need to look it up from the loaded spec using the route path
52-
const apiSpec = getSpec().spec
53-
const routePath = (req.openapi as any)?.openApiRoute || req.path
54-
const method = req.method.toLowerCase()
55-
56-
console.log('DEBUG authz - routePath:', routePath, 'method:', method)
57-
console.log('DEBUG authz - Looking for x-aclSchema at:', `paths['${routePath}']['${method}']['x-aclSchema']`)
58-
console.log('DEBUG authz - Available paths:', Object.keys(apiSpec.paths || {}))
59-
60-
const schema: string =
61-
get(apiSpec, `paths['${routePath}']['${method}']['x-aclSchema']`, '') || get(req, 'operationDoc.x-aclSchema', '')
62-
const schemaName = schema.split('/').pop() || null
52+
// Get x-aclSchema from req.openapi.schema (set by express-openapi-validator)
53+
const schemaName = (req.openapi?.schema as any)?.['x-aclSchema'] || null
6354

64-
console.log('DEBUG authz - Found schema:', schema, 'schemaName:', schemaName)
55+
// If there is no RBAC then we allow the request
56+
if (!schemaName) return
6557

66-
// If there is no RBAC then we bail
67-
if (!schemaName) return next()
58+
const apiSpec = getSpec().spec
6859

69-
// init rules for the user
60+
// Initialize rules for the user
7061
authz.init(user)
7162

63+
// Check RBAC permissions
7264
let valid
7365
const isTeamMember = !user.isPlatformAdmin && user.teams.includes(teamId)
7466
if (isTeamMember) {
@@ -81,12 +73,15 @@ export function authorize(req: OpenApiRequestExt, res, next, authz: Authz, repoS
8173
const key = `${action}:${schemaName}`
8274
const permission = permissionMap[key]
8375
valid = permission ? authz.hasSelfService(teamId, permission) : authz.validateWithCasl(action, schemaName, teamId)
84-
} else valid = authz.validateWithCasl(action, schemaName, teamId)
76+
} else {
77+
valid = authz.validateWithCasl(action, schemaName, teamId)
78+
}
8579

8680
if (!valid) {
8781
throw new HttpError(403, `User not allowed to perform "${action}" on "${schemaName}" resource`)
8882
}
8983

84+
// Check ABAC permissions for create/update operations
9085
const schemaToRepoMap: Record<string, string> = {
9186
Service: 'services',
9287
Team: 'teamConfig',
@@ -98,11 +93,9 @@ export function authorize(req: OpenApiRequestExt, res, next, authz: Authz, repoS
9893
Policy: 'policies',
9994
SealedSecret: 'sealedSecrets',
10095
}
101-
const teamSpecificCollections = ['builds', 'services', 'workloads', 'netpols', 'policies', 'sealedSecrets'] // <-- These are fetched per team
96+
const teamSpecificCollections = ['builds', 'services', 'workloads', 'netpols', 'policies', 'sealedSecrets']
10297

103-
// Use openapi.pathParams if available, fallback to params for backward compatibility
104-
const pathParams = req.openapi?.pathParams ?? params
105-
const selector = renameKeys(pathParams)
98+
//TODO lookup if we can remove this
10699
const collectionId = schemaToRepoMap[schemaName]
107100
if (collectionId && ['create', 'update'].includes(action)) {
108101
// Look up x-allow-values from the API spec for ABAC validation
@@ -114,6 +107,8 @@ export function authorize(req: OpenApiRequestExt, res, next, authz: Authz, repoS
114107

115108
if (action === 'update') {
116109
try {
110+
const pathParams = req.openapi?.pathParams ?? req.params
111+
const selector = renameKeys(pathParams)
117112
let collection
118113
if (teamSpecificCollections.includes(collectionId)) {
119114
collection = repoService.getTeamConfigService(teamId).getCollection(collectionId)
@@ -122,41 +117,16 @@ export function authorize(req: OpenApiRequestExt, res, next, authz: Authz, repoS
122117
}
123118
dataOrig = find(collection, selector) || {}
124119
} catch (error) {
125-
debug('Error in authzMiddleware', error)
120+
debug('Error in authorize', error)
126121
}
127122
}
128123

129124
const violatedAttributes = authz.validateWithAbac(action, schemaName, teamId, req.body, dataOrig)
130125
if (violatedAttributes.length > 0) {
131-
return res.status(403).send({
132-
authz: false,
133-
message: `User not allowed to modify the following attributes ${violatedAttributes}" of ${schemaName}" resource`,
134-
})
135-
}
136-
}
137-
// TODO: enable next part later:
138-
//filter response based on abac
139-
// res.json = wrapResponse(
140-
// (obj: Record<string, any>) => authz.filterWithAbac(schemaName, teamId, obj),
141-
// res.json.bind(res),
142-
// )
143-
144-
return next()
145-
}
146-
export function authzMiddleware(authz: Authz): RequestHandler {
147-
return async function nextHandler(req: OpenApiRequestExt, res, next): Promise<any> {
148-
if (req.user) {
149-
req.isSecurityHandler = true
150-
} else {
151-
return next()
152-
}
153-
try {
154-
const otomi: OtomiStack = await getSessionStack(req.user.email)
155-
// Now we call the new helper which derives authz based on the new selfService.teamMembers flags.
156-
req.user.authz = getTeamSelfServiceAuthz(req.user.teams, otomi)
157-
return authorize(req, res, next, authz, otomi.repoService)
158-
} catch (error) {
159-
return next(error)
126+
throw new HttpError(
127+
403,
128+
`User not allowed to modify the following attributes: ${violatedAttributes.join(', ')} of ${schemaName} resource`,
129+
)
160130
}
161131
}
162132
}

src/middleware/security-handlers.ts

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -35,36 +35,15 @@ export async function groupAuthzSecurityHandler(req: Request, scopes: string[],
3535
const otomiStack = await getSessionStack(extReq.user.email)
3636
extReq.user.authz = getTeamSelfServiceAuthz(extReq.user.teams, otomiStack)
3737

38-
// Perform authorization check using the same logic as authzMiddleware
39-
// We wrap it in a Promise to handle the middleware's async callback pattern
40-
return new Promise((resolve, reject) => {
41-
const mockRes = {} as Response
42-
const mockNext = (error?: any) => {
43-
if (error) {
44-
// Convert HttpError to security handler error format
45-
const status = error.statusCode || 403
46-
const message = error.message || 'Forbidden'
47-
reject({ status, message })
48-
} else {
49-
resolve(true)
50-
}
51-
}
52-
53-
try {
54-
// Create authz instance and call authorize
55-
const authz = new Authz(getSpec().spec)
56-
const result = authorize(extReq, mockRes, mockNext, authz, otomiStack.repoService)
57-
58-
// authorize returns next() or throws, handle the sync return case
59-
if (result === undefined) {
60-
// If authorize called next() synchronously without error, we're authorized
61-
resolve(true)
62-
}
63-
} catch (error: any) {
64-
// authorize threw an error (e.g., HttpError for 403)
65-
const status = error.statusCode || error.status || 403
66-
const message = error.publicMessage || error.message || 'Forbidden'
67-
reject({ status, message })
68-
}
69-
})
38+
// Perform authorization check
39+
try {
40+
const authz = new Authz(getSpec().spec)
41+
authorize(extReq, authz, otomiStack.repoService)
42+
return true
43+
} catch (error: any) {
44+
// authorize threw an error (e.g., HttpError for 403)
45+
const status = error.statusCode || error.status || 403
46+
const message = error.publicMessage || error.message || 'Forbidden'
47+
throw { status, message }
48+
}
7049
}

0 commit comments

Comments
 (0)