feat(server): add switchable admin authz override (#1378)
* feat(server): add switchable admin authz override * fix(server): make sure tests work with the new admin override * feat(server authz): make sure to add all requested roles to server admins in admin override mode
This commit is contained in:
Родитель
ae79a48eb0
Коммит
40a6701799
|
@ -2,6 +2,8 @@
|
|||
const expect = require('chai').expect
|
||||
|
||||
const { beforeEachContext } = require('@/test/hooks')
|
||||
const { createStream } = require('@/modules/core/services/streams')
|
||||
const { createUser } = require('@/modules/core/services/users')
|
||||
|
||||
const {
|
||||
validateServerRole,
|
||||
|
@ -9,6 +11,7 @@ const {
|
|||
authorizeResolver
|
||||
} = require('@/modules/shared')
|
||||
const { buildContext } = require('@/modules/shared/middleware')
|
||||
const { ForbiddenError } = require('apollo-server-express')
|
||||
|
||||
describe('Generic AuthN & AuthZ controller tests', () => {
|
||||
before(async () => {
|
||||
|
@ -90,4 +93,102 @@ describe('Generic AuthN & AuthZ controller tests', () => {
|
|||
})
|
||||
.catch((err) => expect('Unknown role: streams:read').to.equal(err.message))
|
||||
})
|
||||
|
||||
describe('Authorize resolver ', () => {
|
||||
const myStream = {
|
||||
name: 'My Stream 2',
|
||||
isPublic: true
|
||||
}
|
||||
const notMyStream = {
|
||||
name: 'Not My Stream 1',
|
||||
isPublic: false
|
||||
}
|
||||
const serverOwner = {
|
||||
name: 'Itsa Me',
|
||||
email: 'me@gmail.com',
|
||||
password: 'sn3aky-1337-b1m'
|
||||
}
|
||||
const otherGuy = {
|
||||
name: 'Some Other DUde',
|
||||
email: 'otherguy@gmail.com',
|
||||
password: 'sn3aky-1337-b1m'
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
// Seeding
|
||||
await Promise.all([
|
||||
createUser(serverOwner).then((id) => (serverOwner.id = id)),
|
||||
createUser(otherGuy).then((id) => (otherGuy.id = id))
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
createStream({ ...myStream, ownerId: serverOwner.id }).then(
|
||||
(id) => (myStream.id = id)
|
||||
),
|
||||
createStream({ ...notMyStream, ownerId: otherGuy.id }).then(
|
||||
(id) => (notMyStream.id = id)
|
||||
)
|
||||
])
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env.ADMIN_OVERRIDE_ENABLED = 'false'
|
||||
})
|
||||
it('should allow stream:owners to be stream:owners', async () => {
|
||||
const role = await authorizeResolver(
|
||||
serverOwner.id,
|
||||
myStream.id,
|
||||
'stream:contributor'
|
||||
)
|
||||
expect(role).to.equal('stream:owner')
|
||||
})
|
||||
|
||||
it('should get the passed in role for server:admins if override enabled', async () => {
|
||||
process.env.ADMIN_OVERRIDE_ENABLED = 'true'
|
||||
const role = await authorizeResolver(
|
||||
serverOwner.id,
|
||||
myStream.id,
|
||||
'stream:contributor'
|
||||
)
|
||||
expect(role).to.equal('stream:contributor')
|
||||
})
|
||||
|
||||
it('should not allow server:admins to be anything if adminOverride is disabled', async () => {
|
||||
try {
|
||||
await authorizeResolver(serverOwner.id, notMyStream.id, 'stream:contributor')
|
||||
throw 'This should have thrown'
|
||||
} catch (e) {
|
||||
expect(e instanceof ForbiddenError)
|
||||
}
|
||||
})
|
||||
|
||||
it('should allow server:admins to be anything if adminOverride is enabled', async () => {
|
||||
process.env.ADMIN_OVERRIDE_ENABLED = 'true'
|
||||
const role = await authorizeResolver(
|
||||
serverOwner.id,
|
||||
notMyStream.id,
|
||||
'stream:contributor'
|
||||
)
|
||||
expect(role).to.equal('stream:contributor')
|
||||
})
|
||||
|
||||
it('should not allow server:users to be anything if adminOverride is disabled', async () => {
|
||||
try {
|
||||
await authorizeResolver(otherGuy.id, myStream.id, 'stream:contributor')
|
||||
throw 'This should have thrown'
|
||||
} catch (e) {
|
||||
expect(e instanceof ForbiddenError)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not allow server:users to be anything if adminOverride is enabled', async () => {
|
||||
process.env.ADMIN_OVERRIDE_ENABLED = 'true'
|
||||
try {
|
||||
await authorizeResolver(otherGuy.id, myStream.id, 'stream:contributor')
|
||||
throw 'This should have thrown'
|
||||
} catch (e) {
|
||||
expect(e instanceof ForbiddenError)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
ContextError,
|
||||
BadRequestError
|
||||
} from '@/modules/shared/errors'
|
||||
// import { getbAllRoles } from '../core/services/generic'
|
||||
import { adminOverrideEnabled } from '@/modules/shared/helpers/envHelper'
|
||||
|
||||
interface AuthResult {
|
||||
authorized: boolean
|
||||
|
@ -220,6 +220,12 @@ export const contextRequiresStream =
|
|||
}
|
||||
}
|
||||
|
||||
export const allowForServerAdmins: AuthPipelineFunction = async ({
|
||||
context,
|
||||
authResult
|
||||
}) =>
|
||||
context.role === Roles.Server.Admin ? authSuccess(context) : { context, authResult }
|
||||
|
||||
export const allowForRegisteredUsersOnPublicStreamsEvenWithoutRole: AuthPipelineFunction =
|
||||
async ({ context, authResult }) =>
|
||||
context.auth && context.stream?.isPublic
|
||||
|
@ -269,3 +275,5 @@ export const streamReadPermissions = [
|
|||
contextRequiresStream(getStream as StreamGetter),
|
||||
validateStreamRole({ requiredRole: Roles.Stream.Contributor })
|
||||
]
|
||||
|
||||
if (adminOverrideEnabled()) streamReadPermissions.push(allowForServerAdmins)
|
||||
|
|
|
@ -99,3 +99,7 @@ export function shouldDisableNotificationsConsumption() {
|
|||
export function isSSLServer() {
|
||||
return /^https:\/\//.test(getBaseUrl())
|
||||
}
|
||||
|
||||
export function adminOverrideEnabled() {
|
||||
return process.env.ADMIN_OVERRIDE_ENABLED === 'true'
|
||||
}
|
||||
|
|
|
@ -3,6 +3,11 @@ const Redis = require('ioredis')
|
|||
const knex = require(`@/db/knex`)
|
||||
const { ForbiddenError, ApolloError } = require('apollo-server-express')
|
||||
const { RedisPubSub } = require('graphql-redis-subscriptions')
|
||||
const { ServerAcl: ServerAclSchema } = require('@/modules/core/dbSchema')
|
||||
const ServerAcl = () => ServerAclSchema.knex()
|
||||
|
||||
const { Roles } = require('@speckle/shared')
|
||||
const { adminOverrideEnabled } = require('@/modules/shared/helpers/envHelper')
|
||||
|
||||
const StreamPubsubEvents = Object.freeze({
|
||||
UserStreamAdded: 'USER_STREAM_ADDED',
|
||||
|
@ -85,6 +90,11 @@ async function authorizeResolver(userId, resourceId, requiredRole) {
|
|||
|
||||
if (!role) throw new ApolloError('Unknown role: ' + requiredRole)
|
||||
|
||||
if (adminOverrideEnabled()) {
|
||||
const serverRoles = await ServerAcl().select('role').where({ userId })
|
||||
if (serverRoles.map((r) => r.role).includes(Roles.Server.Admin)) return requiredRole
|
||||
}
|
||||
|
||||
try {
|
||||
const { isPublic } = await knex(role.resourceTarget)
|
||||
.select('isPublic')
|
||||
|
@ -107,7 +117,7 @@ async function authorizeResolver(userId, resourceId, requiredRole) {
|
|||
userAclEntry.role = roles.find((r) => r.name === userAclEntry.role)
|
||||
|
||||
if (userAclEntry.role.weight >= role.weight) return userAclEntry.role.name
|
||||
else throw new ForbiddenError('You are not authorized.')
|
||||
throw new ForbiddenError('You are not authorized.')
|
||||
}
|
||||
|
||||
const Scopes = () => knex('scopes')
|
||||
|
@ -122,10 +132,10 @@ async function registerOrUpdateScope(scope) {
|
|||
return
|
||||
}
|
||||
|
||||
const Roles = () => knex('user_roles')
|
||||
const UserRoles = () => knex('user_roles')
|
||||
async function registerOrUpdateRole(role) {
|
||||
await knex.raw(
|
||||
`${Roles()
|
||||
`${UserRoles()
|
||||
.insert(role)
|
||||
.toString()} on conflict (name) do update set weight = ?, description = ?, "resourceTarget" = ? `,
|
||||
[role.weight, role.description, role.resourceTarget]
|
||||
|
|
|
@ -7,7 +7,8 @@ const {
|
|||
validateScope,
|
||||
contextRequiresStream,
|
||||
allowForAllRegisteredUsersOnPublicStreamsWithPublicComments,
|
||||
allowForRegisteredUsersOnPublicStreamsEvenWithoutRole
|
||||
allowForRegisteredUsersOnPublicStreamsEvenWithoutRole,
|
||||
allowForServerAdmins
|
||||
} = require('@/modules/shared/authz')
|
||||
const {
|
||||
ForbiddenError: SFE,
|
||||
|
@ -16,6 +17,7 @@ const {
|
|||
UnauthorizedError,
|
||||
ContextError
|
||||
} = require('@/modules/shared/errors')
|
||||
const { Roles } = require('@speckle/shared')
|
||||
|
||||
describe('AuthZ @shared', () => {
|
||||
describe('Auth pipeline', () => {
|
||||
|
@ -267,6 +269,18 @@ describe('AuthZ @shared', () => {
|
|||
})
|
||||
})
|
||||
describe('Escape hatches', () => {
|
||||
describe('Admin override', () => {
|
||||
it('server:admins get authSuccess', async () => {
|
||||
const input = { context: { role: Roles.Server.Admin }, authResult: 'fake' }
|
||||
const result = await allowForServerAdmins(input)
|
||||
expect(result).to.deep.equal(authSuccess(input.context))
|
||||
})
|
||||
it('server:users get the previous authResult', async () => {
|
||||
const input = { context: { role: Roles.Server.User }, authResult: 'fake' }
|
||||
const result = await allowForServerAdmins(input)
|
||||
expect(result).to.deep.equal(input)
|
||||
})
|
||||
})
|
||||
describe('Allow for public stream no role', () => {
|
||||
it('not public stream, no auth returns same context ', async () => {
|
||||
const input = { context: 'dummy', authResult: 'fake' }
|
||||
|
|
|
@ -133,6 +133,11 @@ spec:
|
|||
- name: DISABLE_FILE_UPLOADS
|
||||
value: "true"
|
||||
{{ end }}
|
||||
|
||||
{{- if .Values.server.adminOverrideEnabled }}
|
||||
- name: ADMIN_OVERRIDE_ENABLED
|
||||
value: "true"
|
||||
{{- end }}
|
||||
|
||||
|
||||
# *** S3 Object Storage ***
|
||||
|
|
|
@ -438,6 +438,14 @@
|
|||
"description": "The number of instances of the Server pod to be deployed within the cluster.",
|
||||
"default": 1
|
||||
},
|
||||
"logLevel": {
|
||||
"type": "string",
|
||||
"description": "The minimum level of logs which will be output"
|
||||
},
|
||||
"adminOverrideEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enables the server side admin authz override"
|
||||
},
|
||||
"sessionSecret": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -364,6 +364,9 @@ server:
|
|||
## @param server.logLevel The minimum level of logs which will be output. Suitable values are trace, debug, info, warn, error, fatal, or silent
|
||||
##
|
||||
logLevel: 'info'
|
||||
|
||||
## @param server.adminOverrideEnabled Enables the server side admin authz override
|
||||
adminOverrideEnabled: false
|
||||
sessionSecret:
|
||||
## @param server.sessionSecret.secretName The name of the Kubernetes Secret containing the Session secret. This is a unique value (can be generated randomly). This is expected to be provided within the Kubernetes cluster as an opaque Kubernetes Secret. Ref: https://kubernetes.io/docs/concepts/configuration/secret/#opaque-secrets
|
||||
##
|
||||
|
|
|
@ -14,6 +14,9 @@ s3:
|
|||
access_key: 'minioadmin'
|
||||
create_bucket: 'true'
|
||||
|
||||
server:
|
||||
adminOverrideEnabled: true
|
||||
|
||||
cert_manager_issuer: ~
|
||||
enable_prometheus_monitoring: true
|
||||
file_size_limit_mb: 300
|
||||
|
|
Загрузка…
Ссылка в новой задаче