feat(backend): implemented centralized RBAC checks
This commit is contained in:
parent
080986294b
commit
19a81a0aed
10 changed files with 77 additions and 39 deletions
|
@ -105,3 +105,22 @@ model Set {
|
|||
|
||||
@@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
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ const baseConfig = {
|
|||
// Config
|
||||
api,
|
||||
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,
|
||||
},
|
||||
avatars: {
|
||||
|
@ -73,10 +73,15 @@ const baseConfig = {
|
|||
port,
|
||||
roles: {
|
||||
levels: {
|
||||
readNone: 0,
|
||||
readSome: 1,
|
||||
readOnly: 2,
|
||||
writeSome: 3,
|
||||
user: 4,
|
||||
bughunter: 5,
|
||||
support: 7,
|
||||
admin: 8,
|
||||
curator: 5,
|
||||
bughunter: 6,
|
||||
support: 8,
|
||||
admin: 9,
|
||||
},
|
||||
base: 'user',
|
||||
},
|
||||
|
|
|
@ -15,6 +15,8 @@ import { loadExpressMiddleware, loadPassportMiddleware } from './middleware.mjs'
|
|||
import { encryption } from './utils/crypto.mjs'
|
||||
// Multi-Factor Authentication (MFA)
|
||||
import { mfa } from './utils/mfa.mjs'
|
||||
// Role-Based Access Control (RBAC)
|
||||
import { rbac } from './utils/rbac.mjs'
|
||||
// Email
|
||||
import { mailer } from './utils/email.mjs'
|
||||
// Swagger
|
||||
|
@ -36,6 +38,7 @@ const tools = {
|
|||
...encryption(config.encryption.key),
|
||||
...mfa(config.mfa),
|
||||
...mailer(config),
|
||||
...rbac(config.roles),
|
||||
config,
|
||||
}
|
||||
|
||||
|
|
|
@ -3,15 +3,6 @@ import http from 'passport-http'
|
|||
import jwt from 'passport-jwt'
|
||||
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) {
|
||||
app.use(cors())
|
||||
}
|
||||
|
@ -36,7 +27,7 @@ function loadPassportMiddleware(passport, tools) {
|
|||
return done(null, {
|
||||
...jwt_payload,
|
||||
uid: jwt_payload._id,
|
||||
level: levelFromRole(jwt_payload.role),
|
||||
level: tools.config.roles.levels[jwt_payload.role] || 0,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
|
|
@ -6,6 +6,7 @@ import { UserModel } from './user.mjs'
|
|||
export function ApikeyModel(tools) {
|
||||
this.config = tools.config
|
||||
this.prisma = tools.prisma
|
||||
this.rbac = tools.rbac
|
||||
this.User = new UserModel(tools)
|
||||
|
||||
return this
|
||||
|
@ -48,7 +49,7 @@ ApikeyModel.prototype.verify = async function (key, secret) {
|
|||
}
|
||||
|
||||
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')
|
||||
|
||||
await this.unguardedRead({ id: params.id })
|
||||
|
@ -56,7 +57,7 @@ ApikeyModel.prototype.guardedRead = async function ({ params, user }) {
|
|||
|
||||
if (this.record.userId !== user.uid) {
|
||||
// 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', {
|
||||
|
@ -72,7 +73,7 @@ ApikeyModel.prototype.guardedRead = 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')
|
||||
|
||||
await this.unguardedRead({ id: params.id })
|
||||
|
@ -80,7 +81,7 @@ ApikeyModel.prototype.guardedDelete = async function ({ params, user }) {
|
|||
|
||||
if (this.record.userId !== user.uid) {
|
||||
// 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()
|
||||
|
|
|
@ -6,6 +6,7 @@ export function PatternModel(tools) {
|
|||
this.prisma = tools.prisma
|
||||
this.decrypt = tools.decrypt
|
||||
this.encrypt = tools.encrypt
|
||||
this.rbac = tools.rbac
|
||||
this.encryptedFields = ['data', 'img', 'name', 'notes', 'settings']
|
||||
this.clear = {} // For holding decrypted data
|
||||
|
||||
|
@ -13,7 +14,7 @@ export function PatternModel(tools) {
|
|||
}
|
||||
|
||||
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 (!body.set) return this.setResponse(400, 'setMissing')
|
||||
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
|
||||
*/
|
||||
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')
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
|
@ -116,11 +117,11 @@ PatternModel.prototype.guardedRead = async function ({ params, user }) {
|
|||
* Stores result in this.record
|
||||
*/
|
||||
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')
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
|
@ -204,10 +205,10 @@ PatternModel.prototype.unguardedUpdate = async function (data) {
|
|||
* so we can't be certain it's safe
|
||||
*/
|
||||
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')
|
||||
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')
|
||||
}
|
||||
const data = {}
|
||||
|
@ -248,11 +249,11 @@ PatternModel.prototype.unguardedDelete = async function () {
|
|||
* Removes the pattern - Checks permissions
|
||||
*/
|
||||
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')
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ export function SetModel(tools) {
|
|||
this.prisma = tools.prisma
|
||||
this.decrypt = tools.decrypt
|
||||
this.encrypt = tools.encrypt
|
||||
this.rbac = tools.rbac
|
||||
this.encryptedFields = ['measies', 'img', 'name', 'notes']
|
||||
this.clear = {} // For holding decrypted data
|
||||
|
||||
|
@ -14,7 +15,7 @@ export function SetModel(tools) {
|
|||
}
|
||||
|
||||
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 (!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
|
||||
*/
|
||||
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')
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
|
@ -123,11 +124,11 @@ SetModel.prototype.publicRead = async function ({ params }) {
|
|||
* Stores result in this.record
|
||||
*/
|
||||
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')
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
|
@ -231,10 +232,10 @@ SetModel.prototype.unguardedUpdate = async function (data) {
|
|||
* so we can't be certain it's safe
|
||||
*/
|
||||
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')
|
||||
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')
|
||||
}
|
||||
const data = {}
|
||||
|
@ -287,11 +288,11 @@ SetModel.prototype.unguardedDelete = async function () {
|
|||
* Removes the set - Checks permissions
|
||||
*/
|
||||
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')
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ export function UserModel(tools) {
|
|||
this.decrypt = tools.decrypt
|
||||
this.encrypt = tools.encrypt
|
||||
this.mfa = tools.mfa
|
||||
this.rbac = tools.rbac
|
||||
this.mailer = tools.email
|
||||
this.Confirmation = new ConfirmationModel(tools)
|
||||
this.encryptedFields = ['bio', 'github', 'email', 'initial', 'img', 'mfaSecret']
|
||||
|
@ -69,7 +70,7 @@ UserModel.prototype.cloak = function (data) {
|
|||
* Stores result in this.record
|
||||
*/
|
||||
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')
|
||||
await this.read(where)
|
||||
|
||||
|
@ -481,7 +482,7 @@ UserModel.prototype.unguardedUpdate = async function (data) {
|
|||
* so we can't be certain it's safe
|
||||
*/
|
||||
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')
|
||||
const data = {}
|
||||
// Bio
|
||||
|
@ -600,7 +601,7 @@ UserModel.prototype.guardedUpdate = async function ({ body, user }) {
|
|||
* user-provided data so we can't be certain it's safe
|
||||
*/
|
||||
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 (body.mfa === true && this.record.mfaEnabled === true)
|
||||
return this.setResponse(400, 'mfaActive')
|
||||
|
|
|
@ -3,6 +3,7 @@ import { usersRoutes } from './users.mjs'
|
|||
import { setsRoutes } from './sets.mjs'
|
||||
import { patternsRoutes } from './patterns.mjs'
|
||||
import { confirmationsRoutes } from './confirmations.mjs'
|
||||
//import { curatedSetsRoutes } from './curated-sets.mjs'
|
||||
|
||||
export const routes = {
|
||||
apikeysRoutes,
|
||||
|
@ -10,4 +11,5 @@ export const routes = {
|
|||
setsRoutes,
|
||||
patternsRoutes,
|
||||
confirmationsRoutes,
|
||||
//curatedSetsRoutes,
|
||||
}
|
||||
|
|
14
sites/backend/src/utils/rbac.mjs
Normal file
14
sites/backend/src/utils/rbac.mjs
Normal 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,
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue