mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-09-14 10:52:34 +00:00
305 lines
8.0 KiB
JavaScript
305 lines
8.0 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.
|
|
*/
|
|
|
|
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);
|
|
}
|
|
},
|
|
};
|
|
}
|