/** * Some Notes: This is a friggin complicated piece of code. * * "scope" in this file means "where did this token come from and what is using it", so 99% of the time * the "scope" is going to be "user" because it would be a user token. This is not to be confused with * the "role" which could be "user" or "admin". The scope in fact, could be "worker" or anything else. */ import fs from "node:fs"; import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; import Ajv from "ajv/dist/2020.js"; import _ from "lodash"; import { access as logger } from "../logger.js"; import proxyHostModel from "../models/proxy_host.js"; import TokenModel from "../models/token.js"; import userModel from "../models/user.js"; import permsSchema from "./access/permissions.json" with { type: "json" }; import roleSchema from "./access/roles.json" with { type: "json" }; import errs from "./error.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export default function (tokenString) { const Token = TokenModel(); let tokenData = null; let initialised = false; const objectCache = {}; let allowInternalAccess = false; let userRoles = []; let permissions = {}; /** * Loads the Token object from the token string * * @returns {Promise} */ this.init = async () => { if (initialised) { return; } if (!tokenString) { throw new errs.PermissionError("Permission Denied"); } tokenData = await Token.load(tokenString); // At this point we need to load the user from the DB and make sure they: // - exist (and not soft deleted) // - still have the appropriate scopes for this token // This is only required when the User ID is supplied or if the token scope has `user` if ( tokenData.attrs.id || (typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, "user") !== -1) ) { // Has token user id or token user scope const user = await userModel .query() .where("id", tokenData.attrs.id) .andWhere("is_deleted", 0) .andWhere("is_disabled", 0) .allowGraph("[permissions]") .withGraphFetched("[permissions]") .first(); if (user) { // make sure user has all scopes of the token // The `user` role is not added against the user row, so we have to just add it here to get past this check. user.roles.push("user"); let ok = true; _.forEach(tokenData.scope, (scope_item) => { if (_.indexOf(user.roles, scope_item) === -1) { ok = false; } }); if (!ok) { throw new errs.AuthError("Invalid token scope for User"); } initialised = true; userRoles = user.roles; permissions = user.permissions; } else { throw new errs.AuthError("User cannot be loaded for Token"); } } initialised = true; }; /** * Fetches the object ids from the database, only once per object type, for this token. * This only applies to USER token scopes, as all other tokens are not really bound * by object scopes * * @param {String} objectType * @returns {Promise} */ this.loadObjects = async (objectType) => { let objects = null; if (Token.hasScope("user")) { if (typeof tokenData.attrs.id === "undefined" || !tokenData.attrs.id) { throw new errs.AuthError("User Token supplied without a User ID"); } const tokenUserId = tokenData.attrs.id ? tokenData.attrs.id : 0; let query; if (typeof objectCache[objectType] !== "undefined") { objects = objectCache[objectType]; } else { switch (objectType) { // USERS - should only return yourself case "users": objects = tokenUserId ? [tokenUserId] : []; break; // Proxy Hosts case "proxy_hosts": { query = proxyHostModel.query().select("id").andWhere("is_deleted", 0); if (permissions.visibility === "user") { query.andWhere("owner_user_id", tokenUserId); } const rows = await query(); objects = []; _.forEach(rows, (ruleRow) => { result.push(ruleRow.id); }); // enum should not have less than 1 item if (!objects.length) { objects.push(0); } break; } } objectCache[objectType] = objects; } } return objects; }; /** * Creates a schema object on the fly with the IDs and other values required to be checked against the permissionSchema * * @param {String} permissionLabel * @returns {Object} */ this.getObjectSchema = async (permissionLabel) => { const baseObjectType = permissionLabel.split(":").shift(); const schema = { $id: "objects", description: "Actor Properties", type: "object", additionalProperties: false, properties: { user_id: { anyOf: [ { type: "number", enum: [Token.get("attrs").id], }, ], }, scope: { type: "string", pattern: `^${Token.get("scope")}$`, }, }, }; const result = await this.loadObjects(baseObjectType); if (typeof result === "object" && result !== null) { schema.properties[baseObjectType] = { type: "number", enum: result, minimum: 1, }; } else { schema.properties[baseObjectType] = { type: "number", minimum: 1, }; } return schema; }; // here: return { token: Token, /** * * @param {Boolean} [allowInternal] * @returns {Promise} */ load: async (allowInternal) => { if (tokenString) { return await Token.load(tokenString); } allowInternalAccess = allowInternal; return allowInternal || null; }, reloadObjects: this.loadObjects, /** * * @param {String} permission * @param {*} [data] * @returns {Promise} */ can: async (permission, data) => { if (allowInternalAccess === true) { return true; } try { await this.init(); const objectSchema = await this.getObjectSchema(permission); const dataSchema = { [permission]: { data: data, scope: Token.get("scope"), roles: userRoles, permission_visibility: permissions.visibility, permission_proxy_hosts: permissions.proxy_hosts, permission_redirection_hosts: permissions.redirection_hosts, permission_dead_hosts: permissions.dead_hosts, permission_streams: permissions.streams, permission_access_lists: permissions.access_lists, permission_certificates: permissions.certificates, }, }; const permissionSchema = { $async: true, $id: "permissions", type: "object", additionalProperties: false, properties: {}, }; const rawData = fs.readFileSync(`${__dirname}/access/${permission.replace(/:/gim, "-")}.json`, { encoding: "utf8", }); permissionSchema.properties[permission] = JSON.parse(rawData); const ajv = new Ajv({ verbose: true, allErrors: true, breakOnError: true, coerceTypes: true, schemas: [roleSchema, permsSchema, objectSchema, permissionSchema], }); const valid = ajv.validate("permissions", dataSchema); return valid && dataSchema[permission]; } catch (err) { err.permission = permission; err.permission_data = data; logger.error(permission, data, err.message); throw errs.PermissionError("Permission Denied", err); } }, }; }