123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- /**
- * 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.
- *
- *
- */
- const _ = require('lodash');
- const logger = require('../logger').access;
- const Ajv = require('ajv/dist/2020');
- const error = require('./error');
- const userModel = require('../models/user');
- const proxyHostModel = require('../models/proxy_host');
- const TokenModel = require('../models/token');
- const roleSchema = require('./access/roles.json');
- const permsSchema = require('./access/permissions.json');
- module.exports = function (token_string) {
- let Token = new TokenModel();
- let token_data = null;
- let initialised = false;
- let 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 error.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 error.AuthError('Invalid token scope for User');
- } else {
- initialised = true;
- user_roles = user.roles;
- permissions = user.permissions;
- }
- } else {
- throw new error.AuthError('User cannot be loaded for Token');
- }
- });
- } else {
- 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 error.AuthError('User Token supplied without a User ID'));
- } else {
- let 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) => {
- let 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) => {
- let base_object_type = permission_label.split(':').shift();
- let 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(function (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: (permission, data) => {
- if (allow_internal_access === true) {
- return Promise.resolve(true);
- //return true;
- } else {
- return this.init()
- .then(() => {
- // Initialised, token decoded ok
- return this.getObjectSchema(permission)
- .then((objectSchema) => {
- const data_schema = {
- [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
- }
- };
- let permissionSchema = {
- $async: true,
- $id: 'permissions',
- additionalProperties: false,
- properties: {}
- };
- permissionSchema.properties[permission] = require('./access/' + permission.replace(/:/gim, '-') + '.json');
- const ajv = new Ajv({
- verbose: true,
- allErrors: true,
- missingRefs: 'fail',
- breakOnError: true,
- coerceTypes: true,
- schemas: [
- roleSchema,
- permsSchema,
- objectSchema,
- permissionSchema
- ]
- });
- return ajv.validate('permissions', data_schema)
- .then(() => {
- return data_schema[permission];
- });
- });
- })
- .catch((err) => {
- err.permission = permission;
- err.permission_data = data;
- logger.error(permission, data, err.message);
- throw new error.PermissionError('Permission Denied', err);
- });
- }
- }
- };
- };
|