bb0f4bfa62
* Fix wrapping when too many hosts are shown (#207) * Update npm packages, fixes CVE-2019-10757 * Revert some breaking packages * Major overhaul - Docker buildx support in CI - Cypress API Testing in CI - Restructured folder layout (insert clean face meme) - Added Swagger documentation and validate API against that (to be completed) - Use common base image for all supported archs, which includes updated nginx with ipv6 support - Updated certbot and changes required for it - Large amount of Hosts names will wrap in UI - Updated packages for frontend - Version bump 2.1.0 * Updated documentation * Fix JWT expire time going crazy. Now set to 1day * Backend JS formatting rules * Remove v1 importer, I doubt anyone is using v1 anymore * Added backend formatting rules and enforce them in Jenkins builds * Fix CI, doesn't need a tty * Thanks bcrypt. Why can't you just be normal. * Cleanup after syntax check Co-authored-by: Marcelo Castagna <margaale@users.noreply.github.com>
315 lines
8.7 KiB
JavaScript
315 lines
8.7 KiB
JavaScript
/**
|
|
* 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 validator = require('ajv');
|
|
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)
|
|
.allowEager('[permissions]')
|
|
.eager('[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',
|
|
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
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) => {
|
|
let 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 = {
|
|
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
$async: true,
|
|
$id: 'permissions',
|
|
additionalProperties: false,
|
|
properties: {}
|
|
};
|
|
|
|
permissionSchema.properties[permission] = require('./access/' + permission.replace(/:/gim, '-') + '.json');
|
|
|
|
// logger.info('objectSchema', JSON.stringify(objectSchema, null, 2));
|
|
// logger.info('permissionSchema', JSON.stringify(permissionSchema, null, 2));
|
|
// logger.info('data_schema', JSON.stringify(data_schema, null, 2));
|
|
|
|
let ajv = validator({
|
|
verbose: true,
|
|
allErrors: true,
|
|
format: 'full',
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
};
|