Skip to content
4 changes: 2 additions & 2 deletions src/authz.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,14 @@ describe('Schema collection wise permissions', () => {
test('A team can doSomething', () => {
const authz = new Authz(spec).init(sessionTeam)
sessionTeam.authz = { teamA: { deniedAttributes: { Team: ['a', 'b'] } } }
expect(() => authz.hasSelfService('teamA', 'Team', 'doSomething')).not.toThrow()
expect(() => authz.hasSelfService('teamA', 'doSomething')).not.toThrow()
sessionTeam.authz = {}
})

test('A team cannot doSomething', () => {
const authz = new Authz(spec).init(sessionTeam)
sessionTeam.authz = { teamA: { deniedAttributes: { Team: ['a', 'b', 'doSomething'] } } }
expect(() => authz.hasSelfService('teamA', 'Team', 'doSomething')).not.toThrow()
expect(() => authz.hasSelfService('teamA', 'doSomething')).not.toThrow()
sessionTeam.authz = {}
})
})
82 changes: 39 additions & 43 deletions src/authz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,7 @@
import { Ability, Subject, subject } from '@casl/ability'
import Debug from 'debug'
import { each, forIn, get, isEmpty, isEqual, omit, set } from 'lodash'
import {
Acl,
AclAction,
OpenAPIDoc,
PermissionSchema,
Schema,
SessionUser,
TeamAuthz,
UserAuthz,
} from 'src/otomi-models'
import { Acl, AclAction, OpenAPIDoc, Schema, SessionUser, TeamAuthz, UserAuthz } from 'src/otomi-models'
import OtomiStack from 'src/otomi-stack'
import { extract, flattenObject } from 'src/utils'

Expand Down Expand Up @@ -72,9 +63,6 @@ export function isValidAuthzSpec(apiDoc: OpenAPIDoc): boolean {
const { schemas } = apiDoc.components
forIn(schemas, (schema: Schema, schemaName: string) => {
debug(`loading rules for ${schemaName} schema`)
// @ts-ignore
// eslint-disable-next-line no-param-reassign

if (schema.type === 'array') {
if (schema['x-acl']) {
err.concat(
Expand Down Expand Up @@ -128,7 +116,6 @@ export const loadSpecRules = (apiDoc: OpenAPIDoc): any => {
const { schemas } = apiDoc.components

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

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

export default class Authz {
user: SessionUser

specRules: Record<string, Schema>

// TODO: replace Ability as it's deprecated
rbac: Ability

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

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

hasSelfService = (teamId: string, schema, attribute: string) => {
const deniedAttributes = get(this.user.authz, `${teamId}.deniedAttributes.${schema}`, []) as Array<string>
if (deniedAttributes.includes(attribute)) return false
return true
hasSelfService = (teamId: string, attribute: string): boolean => {
const deniedAttributes = get(this.user.authz, `${teamId}.deniedAttributes.teamMembers`, []) as Array<string>
return !deniedAttributes.includes(attribute)
}
}

export const getTeamSelfServiceAuthz = (
teams: Array<string>,
schema: PermissionSchema,
otomi: OtomiStack,
): UserAuthz => {
export const getTeamSelfServiceAuthz = (teams: Array<string>, otomi: OtomiStack): UserAuthz => {
const permissionMap: UserAuthz = {}

teams.forEach((teamId) => {
const authz: TeamAuthz = {} as TeamAuthz
Object.keys(schema.properties).forEach((propName) => {
const possiblePermissions = schema.properties[propName].items.enum
set(authz, `deniedAttributes.${propName}`, [])
authz.deniedAttributes[propName] = possiblePermissions.filter((name) => {
const flags = get(otomi.getTeamSelfServiceFlags(teamId), propName, [])
return !flags.includes(name)
// Initialize the team authorization object.
const authz: TeamAuthz = { deniedAttributes: {} }

// Retrieve the selfService flags for the team.
// Expected shape: { teamMembers: { createServices: boolean, editSecurityPolicies: boolean, ... } }
const selfServiceFlags = otomi.getTeamSelfServiceFlags(teamId)?.teamMembers

// Initialize deniedAttributes for teamMembers as an empty array.
authz.deniedAttributes.teamMembers = []

if (selfServiceFlags) {
// For each permission, if its flag is false then add it to the denied list.
Object.entries(selfServiceFlags).forEach(([permissionName, allowed]) => {
if (!allowed) {
authz.deniedAttributes.teamMembers.push(permissionName)
}
})
if (propName === 'team') authz.deniedAttributes.team.push('selfService')
})
} else {
// Fallback: if no selfService data is found, deny all permissions.
authz.deniedAttributes.teamMembers = [
'createServices',
'editSecurityPolicies',
'useCloudShell',
'downloadKubeconfig',
'downloadDockerLogin',
]
}

permissionMap[teamId] = authz
})

return permissionMap
}
27 changes: 12 additions & 15 deletions src/middleware/authz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { RequestHandler } from 'express'
import { find } from 'lodash'
import get from 'lodash/get'
import Authz, { getTeamSelfServiceAuthz } from 'src/authz'
import { OpenApiRequestExt, PermissionSchema, TeamSelfService } from 'src/otomi-models'
import { OpenApiRequestExt } from 'src/otomi-models'
import OtomiStack from 'src/otomi-stack'
import { cleanEnv } from 'src/validators'
import { RepoService } from '../services/RepoService'
Expand Down Expand Up @@ -53,13 +53,11 @@ export function authorize(req: OpenApiRequestExt, res, next, authz: Authz, repoS
authz.init(user)

let valid
if (action === 'read' && schemaName === 'Kubecfg')
valid = authz.hasSelfService(teamId, 'access', 'downloadKubeConfig')
if (action === 'read' && schemaName === 'Kubecfg') valid = authz.hasSelfService(teamId, 'downloadKubeconfig')
else if (action === 'read' && schemaName === 'DockerConfig')
valid = authz.hasSelfService(teamId, 'access', 'downloadDockerConfig')
else if (action === 'create' && schemaName === 'Cloudtty') valid = authz.hasSelfService(teamId, 'access', 'shell')
else if (action === 'update' && schemaName === 'Policy')
valid = authz.hasSelfService(teamId, 'policies', 'edit policies')
valid = authz.hasSelfService(teamId, 'downloadDockerLogin')
else if (action === 'create' && schemaName === 'Cloudtty') valid = authz.hasSelfService(teamId, 'useCloudShell')
else if (action === 'update' && schemaName === 'Policy') valid = authz.hasSelfService(teamId, 'editSecurityPolicies')
else valid = authz.validateWithCasl(action, schemaName, teamId)
const env = cleanEnv({})
// TODO: Debug purpose only for removal of license
Expand Down Expand Up @@ -133,18 +131,17 @@ export function authorize(req: OpenApiRequestExt, res, next, authz: Authz, repoS

return next()
}

export function authzMiddleware(authz: Authz): RequestHandler {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
return async function nextHandler(req: OpenApiRequestExt, res, next): Promise<any> {
if (req.user) req.isSecurityHandler = true
else return next()
if (req.user) {
req.isSecurityHandler = true
} else {
return next()
}
const otomi: OtomiStack = await getSessionStack(req.user.email)
req.user.authz = getTeamSelfServiceAuthz(
req.user.teams,
req.apiDoc.components.schemas.TeamSelfService as TeamSelfService as PermissionSchema,
otomi,
)
// Now we call the new helper which derives authz based on the new selfService.teamMembers flags.
req.user.authz = getTeamSelfServiceAuthz(req.user.teams, otomi)
return authorize(req, res, next, authz, otomi.repoService)
}
}
Loading
Loading