mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-31 07:43:33 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			279 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			279 lines
		
	
	
		
			7.1 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 (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;
 | |
| 
 | |
| 			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": {
 | |
| 						const 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) => {
 | |
| 							objects.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);
 | |
| 			}
 | |
| 		},
 | |
| 	};
 | |
| }
 |