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:
Gergő Jedlicska 2023-02-17 16:31:06 +01:00 коммит произвёл GitHub
Родитель ae79a48eb0
Коммит 40a6701799
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 161 добавлений и 5 удалений

Просмотреть файл

@ -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