| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- /**
- * 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 (token_string) {
- const Token = TokenModel();
- let token_data = null;
- let initialised = false;
- const object_cache = {};
- let allow_internal_access = false;
- let user_roles = [];
- let permissions = {};
- /**
- * Loads the Token object from the token string
- *
- * @returns {Promise}
- */
- this.init = () => {
- return new Promise((resolve, reject) => {
- if (initialised) {
- resolve();
- } else if (!token_string) {
- reject(new errs.PermissionError("Permission Denied"));
- } else {
- resolve(
- Token.load(token_string).then((data) => {
- token_data = data;
- // 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 (
- token_data.attrs.id ||
- (typeof token_data.scope !== "undefined" &&
- _.indexOf(token_data.scope, "user") !== -1)
- ) {
- // Has token user id or token user scope
- return userModel
- .query()
- .where("id", token_data.attrs.id)
- .andWhere("is_deleted", 0)
- .andWhere("is_disabled", 0)
- .allowGraph("[permissions]")
- .withGraphFetched("[permissions]")
- .first()
- .then((user) => {
- 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 is_ok = true;
- _.forEach(token_data.scope, (scope_item) => {
- if (_.indexOf(user.roles, scope_item) === -1) {
- is_ok = false;
- }
- });
- if (!is_ok) {
- throw new errs.AuthError("Invalid token scope for User");
- }
- initialised = true;
- user_roles = 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} object_type
- * @returns {Promise}
- */
- this.loadObjects = (object_type) => {
- return new Promise((resolve, reject) => {
- if (Token.hasScope("user")) {
- if (
- typeof token_data.attrs.id === "undefined" ||
- !token_data.attrs.id
- ) {
- reject(new errs.AuthError("User Token supplied without a User ID"));
- } else {
- const token_user_id = token_data.attrs.id ? token_data.attrs.id : 0;
- let query;
- if (typeof object_cache[object_type] === "undefined") {
- switch (object_type) {
- // USERS - should only return yourself
- case "users":
- resolve(token_user_id ? [token_user_id] : []);
- break;
- // Proxy Hosts
- case "proxy_hosts":
- query = proxyHostModel
- .query()
- .select("id")
- .andWhere("is_deleted", 0);
- if (permissions.visibility === "user") {
- query.andWhere("owner_user_id", token_user_id);
- }
- resolve(
- query.then((rows) => {
- const result = [];
- _.forEach(rows, (rule_row) => {
- result.push(rule_row.id);
- });
- // enum should not have less than 1 item
- if (!result.length) {
- result.push(0);
- }
- return result;
- }),
- );
- break;
- // DEFAULT: null
- default:
- resolve(null);
- break;
- }
- } else {
- resolve(object_cache[object_type]);
- }
- }
- } else {
- resolve(null);
- }
- }).then((objects) => {
- object_cache[object_type] = 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} permission_label
- * @returns {Object}
- */
- this.getObjectSchema = (permission_label) => {
- const base_object_type = permission_label.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")}$`,
- },
- },
- };
- return this.loadObjects(base_object_type).then((object_result) => {
- if (typeof object_result === "object" && object_result !== null) {
- schema.properties[base_object_type] = {
- type: "number",
- enum: object_result,
- minimum: 1,
- };
- } else {
- schema.properties[base_object_type] = {
- type: "number",
- minimum: 1,
- };
- }
- return schema;
- });
- };
- return {
- token: Token,
- /**
- *
- * @param {Boolean} [allow_internal]
- * @returns {Promise}
- */
- load: (allow_internal) => {
- return new Promise((resolve /*, reject*/) => {
- if (token_string) {
- resolve(Token.load(token_string));
- } else {
- allow_internal_access = allow_internal;
- resolve(allow_internal_access || null);
- }
- });
- },
- reloadObjects: this.loadObjects,
- /**
- *
- * @param {String} permission
- * @param {*} [data]
- * @returns {Promise}
- */
- can: async (permission, data) => {
- if (allow_internal_access === true) {
- return true;
- }
- try {
- await this.init();
- const objectSchema = await this.getObjectSchema(permission);
- const dataSchema = {
- [permission]: {
- data: data,
- scope: Token.get("scope"),
- roles: user_roles,
- 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);
- }
- },
- };
- }
|