mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-31 07:43:33 +00:00 
			
		
		
		
	v2.1.0 (#293)
* 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>
This commit is contained in:
		
							
								
								
									
										314
									
								
								backend/lib/access.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										314
									
								
								backend/lib/access.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,314 @@ | ||||
| /** | ||||
|  * 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); | ||||
| 					}); | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user