1
0
Fork 0

feat(backend): implemented centralized RBAC checks

This commit is contained in:
joostdecock 2023-05-06 12:52:26 +02:00
parent 080986294b
commit 19a81a0aed
10 changed files with 77 additions and 39 deletions

View file

@ -105,3 +105,22 @@ model Set {
@@index([userId]) @@index([userId])
} }
model CuratedSet {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
img String?
nameDe String @default("")
nameEn String @default("")
nameEs String @default("")
nameFr String @default("")
nameNl String @default("")
notesDe String @default("")
notesEn String @default("")
notesEs String @default("")
notesFr String @default("")
notesNl String @default("")
measies String @default("{}")
updatedAt DateTime @updatedAt
}

View file

@ -45,7 +45,7 @@ const baseConfig = {
// Config // Config
api, api,
apikeys: { apikeys: {
levels: [0, 1, 2, 3, 4, 5, 6, 7, 8], levels: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
expiryMaxSeconds: 365 * 24 * 3600, expiryMaxSeconds: 365 * 24 * 3600,
}, },
avatars: { avatars: {
@ -73,10 +73,15 @@ const baseConfig = {
port, port,
roles: { roles: {
levels: { levels: {
readNone: 0,
readSome: 1,
readOnly: 2,
writeSome: 3,
user: 4, user: 4,
bughunter: 5, curator: 5,
support: 7, bughunter: 6,
admin: 8, support: 8,
admin: 9,
}, },
base: 'user', base: 'user',
}, },

View file

@ -15,6 +15,8 @@ import { loadExpressMiddleware, loadPassportMiddleware } from './middleware.mjs'
import { encryption } from './utils/crypto.mjs' import { encryption } from './utils/crypto.mjs'
// Multi-Factor Authentication (MFA) // Multi-Factor Authentication (MFA)
import { mfa } from './utils/mfa.mjs' import { mfa } from './utils/mfa.mjs'
// Role-Based Access Control (RBAC)
import { rbac } from './utils/rbac.mjs'
// Email // Email
import { mailer } from './utils/email.mjs' import { mailer } from './utils/email.mjs'
// Swagger // Swagger
@ -36,6 +38,7 @@ const tools = {
...encryption(config.encryption.key), ...encryption(config.encryption.key),
...mfa(config.mfa), ...mfa(config.mfa),
...mailer(config), ...mailer(config),
...rbac(config.roles),
config, config,
} }

View file

@ -3,15 +3,6 @@ import http from 'passport-http'
import jwt from 'passport-jwt' import jwt from 'passport-jwt'
import { ApikeyModel } from './models/apikey.mjs' import { ApikeyModel } from './models/apikey.mjs'
const levelFromRole = (role) => {
if (role === 'user') return 4
if (role === 'bughunter') return 5
if (role === 'support') return 6
if (role === 'admin') return 8
return 0
}
function loadExpressMiddleware(app) { function loadExpressMiddleware(app) {
app.use(cors()) app.use(cors())
} }
@ -36,7 +27,7 @@ function loadPassportMiddleware(passport, tools) {
return done(null, { return done(null, {
...jwt_payload, ...jwt_payload,
uid: jwt_payload._id, uid: jwt_payload._id,
level: levelFromRole(jwt_payload.role), level: tools.config.roles.levels[jwt_payload.role] || 0,
}) })
} }
) )

View file

@ -6,6 +6,7 @@ import { UserModel } from './user.mjs'
export function ApikeyModel(tools) { export function ApikeyModel(tools) {
this.config = tools.config this.config = tools.config
this.prisma = tools.prisma this.prisma = tools.prisma
this.rbac = tools.rbac
this.User = new UserModel(tools) this.User = new UserModel(tools)
return this return this
@ -48,7 +49,7 @@ ApikeyModel.prototype.verify = async function (key, secret) {
} }
ApikeyModel.prototype.guardedRead = async function ({ params, user }) { ApikeyModel.prototype.guardedRead = async function ({ params, user }) {
if (user.level < 1) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.readSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking') if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.unguardedRead({ id: params.id }) await this.unguardedRead({ id: params.id })
@ -56,7 +57,7 @@ ApikeyModel.prototype.guardedRead = async function ({ params, user }) {
if (this.record.userId !== user.uid) { if (this.record.userId !== user.uid) {
// Not own key - only admin can do that // Not own key - only admin can do that
if (user.level < 8) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.admin(user)) return this.setResponse(403, 'insufficientAccessLevel')
} }
return this.setResponse(200, 'success', { return this.setResponse(200, 'success', {
@ -72,7 +73,7 @@ ApikeyModel.prototype.guardedRead = async function ({ params, user }) {
} }
ApikeyModel.prototype.guardedDelete = async function ({ params, user }) { ApikeyModel.prototype.guardedDelete = async function ({ params, user }) {
if (user.level < 4) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.user(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking') if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.unguardedRead({ id: params.id }) await this.unguardedRead({ id: params.id })
@ -80,7 +81,7 @@ ApikeyModel.prototype.guardedDelete = async function ({ params, user }) {
if (this.record.userId !== user.uid) { if (this.record.userId !== user.uid) {
// Not own key - only admin can do that // Not own key - only admin can do that
if (user.level < 8) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.admin(user)) return this.setResponse(403, 'insufficientAccessLevel')
} }
await this.unguardedDelete() await this.unguardedDelete()

View file

@ -6,6 +6,7 @@ export function PatternModel(tools) {
this.prisma = tools.prisma this.prisma = tools.prisma
this.decrypt = tools.decrypt this.decrypt = tools.decrypt
this.encrypt = tools.encrypt this.encrypt = tools.encrypt
this.rbac = tools.rbac
this.encryptedFields = ['data', 'img', 'name', 'notes', 'settings'] this.encryptedFields = ['data', 'img', 'name', 'notes', 'settings']
this.clear = {} // For holding decrypted data this.clear = {} // For holding decrypted data
@ -13,7 +14,7 @@ export function PatternModel(tools) {
} }
PatternModel.prototype.guardedCreate = async function ({ body, user }) { PatternModel.prototype.guardedCreate = async function ({ body, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.user(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (Object.keys(body).length < 2) return this.setResponse(400, 'postBodyMissing') if (Object.keys(body).length < 2) return this.setResponse(400, 'postBodyMissing')
if (!body.set) return this.setResponse(400, 'setMissing') if (!body.set) return this.setResponse(400, 'setMissing')
if (typeof body.set !== 'number') return this.setResponse(400, 'setNotNumeric') if (typeof body.set !== 'number') return this.setResponse(400, 'setNotNumeric')
@ -95,11 +96,11 @@ PatternModel.prototype.read = async function (where) {
* Stores result in this.record * Stores result in this.record
*/ */
PatternModel.prototype.guardedRead = async function ({ params, user }) { PatternModel.prototype.guardedRead = async function ({ params, user }) {
if (user.level < 1) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.readSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking') if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) }) await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && user.level < 5) { if (this.record.userId !== user.uid && !this.rbac.bughunter(user)) {
return this.setResponse(403, 'insufficientAccessLevel') return this.setResponse(403, 'insufficientAccessLevel')
} }
@ -116,11 +117,11 @@ PatternModel.prototype.guardedRead = async function ({ params, user }) {
* Stores result in this.record * Stores result in this.record
*/ */
PatternModel.prototype.guardedClone = async function ({ params, user }) { PatternModel.prototype.guardedClone = async function ({ params, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking') if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) }) await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && !this.record.public && user.level < 5) { if (this.record.userId !== user.uid && !this.record.public && !this.rbac.support(support)) {
return this.setResponse(403, 'insufficientAccessLevel') return this.setResponse(403, 'insufficientAccessLevel')
} }
@ -204,10 +205,10 @@ PatternModel.prototype.unguardedUpdate = async function (data) {
* so we can't be certain it's safe * so we can't be certain it's safe
*/ */
PatternModel.prototype.guardedUpdate = async function ({ params, body, user }) { PatternModel.prototype.guardedUpdate = async function ({ params, body, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking') if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) }) await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && user.level < 8) { if (this.record.userId !== user.uid && !this.rbac.admin(user)) {
return this.setResponse(403, 'insufficientAccessLevel') return this.setResponse(403, 'insufficientAccessLevel')
} }
const data = {} const data = {}
@ -248,11 +249,11 @@ PatternModel.prototype.unguardedDelete = async function () {
* Removes the pattern - Checks permissions * Removes the pattern - Checks permissions
*/ */
PatternModel.prototype.guardedDelete = async function ({ params, user }) { PatternModel.prototype.guardedDelete = async function ({ params, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel') if (this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking') if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) }) await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && user.level < 8) { if (this.record.userId !== user.uid && !this.rbac.admin(user)) {
return this.setResponse(403, 'insufficientAccessLevel') return this.setResponse(403, 'insufficientAccessLevel')
} }

View file

@ -7,6 +7,7 @@ export function SetModel(tools) {
this.prisma = tools.prisma this.prisma = tools.prisma
this.decrypt = tools.decrypt this.decrypt = tools.decrypt
this.encrypt = tools.encrypt this.encrypt = tools.encrypt
this.rbac = tools.rbac
this.encryptedFields = ['measies', 'img', 'name', 'notes'] this.encryptedFields = ['measies', 'img', 'name', 'notes']
this.clear = {} // For holding decrypted data this.clear = {} // For holding decrypted data
@ -14,7 +15,7 @@ export function SetModel(tools) {
} }
SetModel.prototype.guardedCreate = async function ({ body, user }) { SetModel.prototype.guardedCreate = async function ({ body, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (Object.keys(body).length < 1) return this.setResponse(400, 'postBodyMissing') if (Object.keys(body).length < 1) return this.setResponse(400, 'postBodyMissing')
if (!body.name || typeof body.name !== 'string') return this.setResponse(400, 'nameMissing') if (!body.name || typeof body.name !== 'string') return this.setResponse(400, 'nameMissing')
@ -86,11 +87,11 @@ SetModel.prototype.read = async function (where) {
* Stores result in this.record * Stores result in this.record
*/ */
SetModel.prototype.guardedRead = async function ({ params, user }) { SetModel.prototype.guardedRead = async function ({ params, user }) {
if (user.level < 1) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.readSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking') if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) }) await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && user.level < 5) { if (this.record.userId !== user.uid && !this.rbac.bughunter(user)) {
return this.setResponse(403, 'insufficientAccessLevel') return this.setResponse(403, 'insufficientAccessLevel')
} }
@ -123,11 +124,11 @@ SetModel.prototype.publicRead = async function ({ params }) {
* Stores result in this.record * Stores result in this.record
*/ */
SetModel.prototype.guardedClone = async function ({ params, user }) { SetModel.prototype.guardedClone = async function ({ params, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking') if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) }) await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && !this.record.public && user.level < 5) { if (this.record.userId !== user.uid && !this.record.public && !this.rbac.support(user)) {
return this.setResponse(403, 'insufficientAccessLevel') return this.setResponse(403, 'insufficientAccessLevel')
} }
@ -231,10 +232,10 @@ SetModel.prototype.unguardedUpdate = async function (data) {
* so we can't be certain it's safe * so we can't be certain it's safe
*/ */
SetModel.prototype.guardedUpdate = async function ({ params, body, user }) { SetModel.prototype.guardedUpdate = async function ({ params, body, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking') if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) }) await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && user.level < 8) { if (this.record.userId !== user.uid && !this.rbac.admin(user)) {
return this.setResponse(403, 'insufficientAccessLevel') return this.setResponse(403, 'insufficientAccessLevel')
} }
const data = {} const data = {}
@ -287,11 +288,11 @@ SetModel.prototype.unguardedDelete = async function () {
* Removes the set - Checks permissions * Removes the set - Checks permissions
*/ */
SetModel.prototype.guardedDelete = async function ({ params, user }) { SetModel.prototype.guardedDelete = async function ({ params, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking') if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) }) await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && user.level < 8) { if (this.record.userId !== user.uid && !this.rbac.admin(user)) {
return this.setResponse(403, 'insufficientAccessLevel') return this.setResponse(403, 'insufficientAccessLevel')
} }

View file

@ -11,6 +11,7 @@ export function UserModel(tools) {
this.decrypt = tools.decrypt this.decrypt = tools.decrypt
this.encrypt = tools.encrypt this.encrypt = tools.encrypt
this.mfa = tools.mfa this.mfa = tools.mfa
this.rbac = tools.rbac
this.mailer = tools.email this.mailer = tools.email
this.Confirmation = new ConfirmationModel(tools) this.Confirmation = new ConfirmationModel(tools)
this.encryptedFields = ['bio', 'github', 'email', 'initial', 'img', 'mfaSecret'] this.encryptedFields = ['bio', 'github', 'email', 'initial', 'img', 'mfaSecret']
@ -69,7 +70,7 @@ UserModel.prototype.cloak = function (data) {
* Stores result in this.record * Stores result in this.record
*/ */
UserModel.prototype.guardedRead = async function (where, { user }) { UserModel.prototype.guardedRead = async function (where, { user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.readSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking') if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read(where) await this.read(where)
@ -481,7 +482,7 @@ UserModel.prototype.unguardedUpdate = async function (data) {
* so we can't be certain it's safe * so we can't be certain it's safe
*/ */
UserModel.prototype.guardedUpdate = async function ({ body, user }) { UserModel.prototype.guardedUpdate = async function ({ body, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking') if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
const data = {} const data = {}
// Bio // Bio
@ -600,7 +601,7 @@ UserModel.prototype.guardedUpdate = async function ({ body, user }) {
* user-provided data so we can't be certain it's safe * user-provided data so we can't be certain it's safe
*/ */
UserModel.prototype.guardedMfaUpdate = async function ({ body, user, ip }) { UserModel.prototype.guardedMfaUpdate = async function ({ body, user, ip }) {
if (user.level < 4) return this.setResponse(403, 'insufficientAccessLevel') if (!this.rbac.user(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking') if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
if (body.mfa === true && this.record.mfaEnabled === true) if (body.mfa === true && this.record.mfaEnabled === true)
return this.setResponse(400, 'mfaActive') return this.setResponse(400, 'mfaActive')

View file

@ -3,6 +3,7 @@ import { usersRoutes } from './users.mjs'
import { setsRoutes } from './sets.mjs' import { setsRoutes } from './sets.mjs'
import { patternsRoutes } from './patterns.mjs' import { patternsRoutes } from './patterns.mjs'
import { confirmationsRoutes } from './confirmations.mjs' import { confirmationsRoutes } from './confirmations.mjs'
//import { curatedSetsRoutes } from './curated-sets.mjs'
export const routes = { export const routes = {
apikeysRoutes, apikeysRoutes,
@ -10,4 +11,5 @@ export const routes = {
setsRoutes, setsRoutes,
patternsRoutes, patternsRoutes,
confirmationsRoutes, confirmationsRoutes,
//curatedSetsRoutes,
} }

View file

@ -0,0 +1,14 @@
/*
* Exporting this closure that makes sure we have access to the
* instantiated config
*/
export const rbac = ({ levels }) => {
const rbacMethods = {}
for (const [name, level] of Object.entries(levels)) {
rbacMethods[name] = (user) => user.level >= level
}
return {
rbac: rbacMethods,
}
}