Skip to content

Commit 46b9091

Browse files
Merge branch 'main' into APL-537-new
2 parents e534b65 + e4b1756 commit 46b9091

5 files changed

Lines changed: 198 additions & 266 deletions

File tree

src/authz.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,14 @@ describe('Schema collection wise permissions', () => {
123123
test('A team can doSomething', () => {
124124
const authz = new Authz(spec).init(sessionTeam)
125125
sessionTeam.authz = { teamA: { deniedAttributes: { Team: ['a', 'b'] } } }
126-
expect(() => authz.hasSelfService('teamA', 'Team', 'doSomething')).not.toThrow()
126+
expect(() => authz.hasSelfService('teamA', 'doSomething')).not.toThrow()
127127
sessionTeam.authz = {}
128128
})
129129

130130
test('A team cannot doSomething', () => {
131131
const authz = new Authz(spec).init(sessionTeam)
132132
sessionTeam.authz = { teamA: { deniedAttributes: { Team: ['a', 'b', 'doSomething'] } } }
133-
expect(() => authz.hasSelfService('teamA', 'Team', 'doSomething')).not.toThrow()
133+
expect(() => authz.hasSelfService('teamA', 'doSomething')).not.toThrow()
134134
sessionTeam.authz = {}
135135
})
136136
})

src/authz.ts

Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,7 @@
22
import { Ability, Subject, subject } from '@casl/ability'
33
import Debug from 'debug'
44
import { each, forIn, get, isEmpty, isEqual, omit, set } from 'lodash'
5-
import {
6-
Acl,
7-
AclAction,
8-
OpenAPIDoc,
9-
PermissionSchema,
10-
Schema,
11-
SessionUser,
12-
TeamAuthz,
13-
UserAuthz,
14-
} from 'src/otomi-models'
5+
import { Acl, AclAction, OpenAPIDoc, Schema, SessionUser, TeamAuthz, UserAuthz } from 'src/otomi-models'
156
import OtomiStack from 'src/otomi-stack'
167
import { extract, flattenObject } from 'src/utils'
178

@@ -72,9 +63,6 @@ export function isValidAuthzSpec(apiDoc: OpenAPIDoc): boolean {
7263
const { schemas } = apiDoc.components
7364
forIn(schemas, (schema: Schema, schemaName: string) => {
7465
debug(`loading rules for ${schemaName} schema`)
75-
// @ts-ignore
76-
// eslint-disable-next-line no-param-reassign
77-
7866
if (schema.type === 'array') {
7967
if (schema['x-acl']) {
8068
err.concat(
@@ -128,7 +116,6 @@ export const loadSpecRules = (apiDoc: OpenAPIDoc): any => {
128116
const { schemas } = apiDoc.components
129117

130118
Object.keys(schemas).forEach((schemaName: string) => {
131-
// debug(`loading rules for ${schemaName} schema`)
132119
const schema: Schema = schemas[schemaName]
133120

134121
if (schema.type === 'array') return
@@ -146,9 +133,8 @@ export const loadSpecRules = (apiDoc: OpenAPIDoc): any => {
146133

147134
export default class Authz {
148135
user: SessionUser
149-
150136
specRules: Record<string, Schema>
151-
137+
// TODO: replace Ability as it's deprecated
152138
rbac: Ability
153139

154140
constructor(apiDoc: OpenAPIDoc) {
@@ -189,10 +175,7 @@ export default class Authz {
189175
// actions like *-any imply that * is also allowed, so exclude those from inversion
190176
const normalized = _actions.map((a) => (a.includes('-any') ? a.slice(0, -4) : a))
191177
allowedAttributeCrudActions
192-
.filter((a) => {
193-
const cond = !(normalized.includes(a) || _actions.includes(`${a}-any`))
194-
return cond
195-
})
178+
.filter((a) => !(normalized.includes(a) || _actions.includes(`${a}-any`)))
196179
.forEach(createRule(schemaName, prop, true))
197180
if (obj.properties) createRules(`${schemaName}.${prop}`, obj)
198181
})
@@ -226,19 +209,18 @@ export default class Authz {
226209
// also check if we are denied by lack of self service
227210
const deniedSelfServiceAttributes = get(
228211
this.user.authz,
229-
`${teamId}.deniedAttributes.${schemaName.toLowerCase()}`,
212+
`${teamId}.deniedAttributes.teamMembers`,
230213
[],
231214
) as Array<string>
232-
// the two above denied lists should be mutually exclusive, because a schema design should not
233-
// have have both self service as well as acl set for the same property, so we can merge the result
215+
// merge denied attributes from both role-based and self-service restrictions
234216
const deniedAttributes = [...deniedRoleAttributes, ...deniedSelfServiceAttributes]
235217

236218
deniedAttributes.forEach((path) => {
237219
const val = get(body, path)
238220
const origVal = get(dataOrig, path)
239-
// undefined value expected for forbidden props, just put back before save
221+
// undefined value expected for forbidden props, so put back original before save
240222
if (val === undefined) set(body, path, origVal)
241-
// value provided which shouldn't happen
223+
// if a value is provided which is not allowed, mark it as violated
242224
else if (!isEqual(val, origVal)) violatedAttributes.push(path)
243225
})
244226
return violatedAttributes
@@ -260,32 +242,46 @@ export default class Authz {
260242
return body.length !== undefined ? ret : ret[0]
261243
}
262244

263-
hasSelfService = (teamId: string, schema, attribute: string) => {
264-
const deniedAttributes = get(this.user.authz, `${teamId}.deniedAttributes.${schema}`, []) as Array<string>
265-
if (deniedAttributes.includes(attribute)) return false
266-
return true
245+
hasSelfService = (teamId: string, attribute: string): boolean => {
246+
const deniedAttributes = get(this.user.authz, `${teamId}.deniedAttributes.teamMembers`, []) as Array<string>
247+
return !deniedAttributes.includes(attribute)
267248
}
268249
}
269250

270-
export const getTeamSelfServiceAuthz = (
271-
teams: Array<string>,
272-
schema: PermissionSchema,
273-
otomi: OtomiStack,
274-
): UserAuthz => {
251+
export const getTeamSelfServiceAuthz = (teams: Array<string>, otomi: OtomiStack): UserAuthz => {
275252
const permissionMap: UserAuthz = {}
276253

277254
teams.forEach((teamId) => {
278-
const authz: TeamAuthz = {} as TeamAuthz
279-
Object.keys(schema.properties).forEach((propName) => {
280-
const possiblePermissions = schema.properties[propName].items.enum
281-
set(authz, `deniedAttributes.${propName}`, [])
282-
authz.deniedAttributes[propName] = possiblePermissions.filter((name) => {
283-
const flags = get(otomi.getTeamSelfServiceFlags(teamId), propName, [])
284-
return !flags.includes(name)
255+
// Initialize the team authorization object.
256+
const authz: TeamAuthz = { deniedAttributes: {} }
257+
258+
// Retrieve the selfService flags for the team.
259+
// Expected shape: { teamMembers: { createServices: boolean, editSecurityPolicies: boolean, ... } }
260+
const selfServiceFlags = otomi.getTeamSelfServiceFlags(teamId)?.teamMembers
261+
262+
// Initialize deniedAttributes for teamMembers as an empty array.
263+
authz.deniedAttributes.teamMembers = []
264+
265+
if (selfServiceFlags) {
266+
// For each permission, if its flag is false then add it to the denied list.
267+
Object.entries(selfServiceFlags).forEach(([permissionName, allowed]) => {
268+
if (!allowed) {
269+
authz.deniedAttributes.teamMembers.push(permissionName)
270+
}
285271
})
286-
if (propName === 'team') authz.deniedAttributes.team.push('selfService')
287-
})
272+
} else {
273+
// Fallback: if no selfService data is found, deny all permissions.
274+
authz.deniedAttributes.teamMembers = [
275+
'createServices',
276+
'editSecurityPolicies',
277+
'useCloudShell',
278+
'downloadKubeconfig',
279+
'downloadDockerLogin',
280+
]
281+
}
282+
288283
permissionMap[teamId] = authz
289284
})
285+
290286
return permissionMap
291287
}

src/middleware/authz.ts

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/* eslint-disable no-param-reassign */
2+
import { debug } from 'console'
23
import { RequestHandler } from 'express'
4+
import { find } from 'lodash'
35
import get from 'lodash/get'
46
import Authz, { getTeamSelfServiceAuthz } from 'src/authz'
5-
import { OpenApiRequestExt, PermissionSchema, TeamSelfService } from 'src/otomi-models'
7+
import { OpenApiRequestExt } from 'src/otomi-models'
68
import OtomiStack from 'src/otomi-stack'
79
import { cleanEnv } from 'src/validators'
8-
import { getSessionStack } from './session'
910
import { RepoService } from '../services/RepoService'
10-
import { debug } from 'console'
11-
import { find } from 'lodash'
11+
import { getSessionStack } from './session'
1212

1313
const HttpMethodMapping: Record<string, string> = {
1414
DELETE: 'delete',
@@ -41,11 +41,8 @@ function renameKeys(obj: Record<string, any>) {
4141
// }
4242

4343
export function authorize(req: OpenApiRequestExt, res, next, authz: Authz, repoService: RepoService): RequestHandler {
44-
const {
45-
params: { teamId },
46-
body,
47-
user,
48-
} = req
44+
const { params, query, body, user } = req
45+
const teamId = params?.teamId ?? query?.teamId
4946
const action = HttpMethodMapping[req.method]
5047
const schema: string = get(req, 'operationDoc.x-aclSchema', '')
5148
const schemaName = schema.split('/').pop() || null
@@ -56,13 +53,11 @@ export function authorize(req: OpenApiRequestExt, res, next, authz: Authz, repoS
5653
authz.init(user)
5754

5855
let valid
59-
if (action === 'read' && schemaName === 'Kubecfg')
60-
valid = authz.hasSelfService(teamId, 'access', 'downloadKubeConfig')
56+
if (action === 'read' && schemaName === 'Kubecfg') valid = authz.hasSelfService(teamId, 'downloadKubeconfig')
6157
else if (action === 'read' && schemaName === 'DockerConfig')
62-
valid = authz.hasSelfService(teamId, 'access', 'downloadDockerConfig')
63-
else if (action === 'create' && schemaName === 'Cloudtty') valid = authz.hasSelfService(teamId, 'access', 'shell')
64-
else if (action === 'update' && schemaName === 'Policy')
65-
valid = authz.hasSelfService(teamId, 'policies', 'edit policies')
58+
valid = authz.hasSelfService(teamId, 'downloadDockerLogin')
59+
else if (action === 'create' && schemaName === 'Cloudtty') valid = authz.hasSelfService(teamId, 'useCloudShell')
60+
else if (action === 'update' && schemaName === 'Policy') valid = authz.hasSelfService(teamId, 'editSecurityPolicies')
6661
else valid = authz.validateWithCasl(action, schemaName, teamId)
6762
const env = cleanEnv({})
6863
// TODO: Debug purpose only for removal of license
@@ -136,18 +131,17 @@ export function authorize(req: OpenApiRequestExt, res, next, authz: Authz, repoS
136131

137132
return next()
138133
}
139-
140134
export function authzMiddleware(authz: Authz): RequestHandler {
141135
// eslint-disable-next-line @typescript-eslint/no-misused-promises
142136
return async function nextHandler(req: OpenApiRequestExt, res, next): Promise<any> {
143-
if (req.user) req.isSecurityHandler = true
144-
else return next()
137+
if (req.user) {
138+
req.isSecurityHandler = true
139+
} else {
140+
return next()
141+
}
145142
const otomi: OtomiStack = await getSessionStack(req.user.email)
146-
req.user.authz = getTeamSelfServiceAuthz(
147-
req.user.teams,
148-
req.apiDoc.components.schemas.TeamSelfService as TeamSelfService as PermissionSchema,
149-
otomi,
150-
)
143+
// Now we call the new helper which derives authz based on the new selfService.teamMembers flags.
144+
req.user.authz = getTeamSelfServiceAuthz(req.user.teams, otomi)
151145
return authorize(req, res, next, authz, otomi.repoService)
152146
}
153147
}

0 commit comments

Comments
 (0)