diff --git a/sites/backend/prisma/schema.prisma b/sites/backend/prisma/schema.prisma index 5f2577c4fc7..d58b243ca7f 100644 --- a/sites/backend/prisma/schema.prisma +++ b/sites/backend/prisma/schema.prisma @@ -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 +} + diff --git a/sites/backend/src/config.mjs b/sites/backend/src/config.mjs index 7e74744c25c..41da446764b 100644 --- a/sites/backend/src/config.mjs +++ b/sites/backend/src/config.mjs @@ -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', }, diff --git a/sites/backend/src/index.mjs b/sites/backend/src/index.mjs index 59e182ce6d5..207a91f3b75 100644 --- a/sites/backend/src/index.mjs +++ b/sites/backend/src/index.mjs @@ -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, } diff --git a/sites/backend/src/middleware.mjs b/sites/backend/src/middleware.mjs index 7ba919b177e..f25589fedf4 100644 --- a/sites/backend/src/middleware.mjs +++ b/sites/backend/src/middleware.mjs @@ -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, }) } ) diff --git a/sites/backend/src/models/apikey.mjs b/sites/backend/src/models/apikey.mjs index d6c5903a548..1983c57afe8 100644 --- a/sites/backend/src/models/apikey.mjs +++ b/sites/backend/src/models/apikey.mjs @@ -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() diff --git a/sites/backend/src/models/pattern.mjs b/sites/backend/src/models/pattern.mjs index 1db22302e6a..78a0163b1b3 100644 --- a/sites/backend/src/models/pattern.mjs +++ b/sites/backend/src/models/pattern.mjs @@ -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') } diff --git a/sites/backend/src/models/set.mjs b/sites/backend/src/models/set.mjs index c8a8e8c455d..2fd0ec9c6f0 100644 --- a/sites/backend/src/models/set.mjs +++ b/sites/backend/src/models/set.mjs @@ -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') } diff --git a/sites/backend/src/models/user.mjs b/sites/backend/src/models/user.mjs index cbe3e29fb38..a821279aed8 100644 --- a/sites/backend/src/models/user.mjs +++ b/sites/backend/src/models/user.mjs @@ -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') diff --git a/sites/backend/src/routes/index.mjs b/sites/backend/src/routes/index.mjs index 84e8fd90286..c2a4a52d7a5 100644 --- a/sites/backend/src/routes/index.mjs +++ b/sites/backend/src/routes/index.mjs @@ -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, } diff --git a/sites/backend/src/utils/rbac.mjs b/sites/backend/src/utils/rbac.mjs new file mode 100644 index 00000000000..d1eed44e4a3 --- /dev/null +++ b/sites/backend/src/utils/rbac.mjs @@ -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, + } +}