UNPKG

verdaccio-mongodb

Version:
404 lines (312 loc) 21 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _commonsApi = require("@verdaccio/commons-api"); var _lruCache = _interopRequireDefault(require("lru-cache")); var _mongoConnector = _interopRequireDefault(require("./mongoConnector.js")); var _helpers = require("./helpers"); var _password = require("./password"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } const cacheOptions = { max: 1000, ttl: 1000 * 60 * 5, allowStale: false, updateAgeOnGet: false, updateAgeOnHas: false }; /** * Custom Verdaccio Authenticate Plugin. */ class AuthMongoDB { constructor(config, options) { var _config$fields, _config$fields2, _config$fields3, _config$fields4, _config$rights, _config$rights2, _config$rights3; _defineProperty(this, "logger", void 0); _defineProperty(this, "config", void 0); _defineProperty(this, "cache", void 0); this.logger = options.logger; this.config = config; // TODO: delete this after testing! // eslint-disable-next-line @typescript-eslint/no-var-requires const pjson = require('../package.json'); this.logger.info(`mongodb: Testing version "${pjson.version}"!`); // Basic configuration check let requiredConfigMissing = false; if (!config.uri) { this.logger.error('mongodb: Required URI was not specified in the config file!'); requiredConfigMissing = true; } if (!config.db) { this.logger.error('mongodb: Required DB was not specified in the config file!'); requiredConfigMissing = true; } if (!this.config.collections) { this.config.collections = {}; } if (!config.collections.users) { this.logger.error('mongodb: Required "users" Collection was not specified in the config file!'); requiredConfigMissing = true; } if (!config.collections.packages) { this.logger.debug('mongodb: The "packages" Collection was not specified in the config file!'); } if (!this.config.fields) { this.config.fields = {}; } if (!((_config$fields = config.fields) === null || _config$fields === void 0 ? void 0 : _config$fields.username)) { this.logger.warn('mongodb: Field username was not specified in the config file! Using default "username"'); this.config.fields.username = 'username'; } if (!((_config$fields2 = config.fields) === null || _config$fields2 === void 0 ? void 0 : _config$fields2.password)) { this.logger.warn('mongodb: Field password was not specified in the config file! Using default "password"'); this.config.fields.password = 'password'; } if (!((_config$fields3 = config.fields) === null || _config$fields3 === void 0 ? void 0 : _config$fields3.usergroups)) { this.logger.warn('mongodb: Field usergroups was not specified in the config file! Using default "usergroups"'); this.config.fields.usergroups = 'usergroups'; } if (!((_config$fields4 = config.fields) === null || _config$fields4 === void 0 ? void 0 : _config$fields4.packagename)) { this.logger.warn('mongodb: Field packagename was not specified in the config file! Using default "packagename"'); this.config.fields.packagename = 'packagename'; } if (!this.config.rights) { this.config.rights = {}; } if (!((_config$rights = config.rights) === null || _config$rights === void 0 ? void 0 : _config$rights.access)) { this.logger.warn('mongodb: Right to access was not specified in the config file! Using default "user"'); this.config.rights.access = 'user'; } if (!((_config$rights2 = config.rights) === null || _config$rights2 === void 0 ? void 0 : _config$rights2.publish)) { this.logger.warn('mongodb: Right to publish was not specified in the config file! Using default "user"'); this.config.rights.publish = 'user'; } if (!((_config$rights3 = config.rights) === null || _config$rights3 === void 0 ? void 0 : _config$rights3.unpublish)) { this.logger.warn('mongodb: Right to unpublish was not specified in the config file! Using default "user"'); this.config.rights.unpublish = 'user'; } if (config.allowAddUser === undefined || config.allowAddUser !== true && config.allowAddUser !== false) { this.logger.warn('mongodb: Optional field allowAddUser was not specified in the config file! Using default "false"'); this.config.allowAddUser = false; } if (!config.countActivity) { this.logger.warn('mongodb: Optional field countActivity was not specified in the config file! Using default "false".'); this.config.countActivity = false; } if (!(config === null || config === void 0 ? void 0 : config.adminGroup)) { this.logger.warn('mongodb: Field adminGroup was not specified in the config file! Using default "__admin__"'); this.config.adminGroup = '__admin__'; } if (!config.cacheTTL) { this.logger.debug('mongodb: Optional field cacheTTL was not specified in the config file! Using default "5 minutes"'); this.config.cacheTTL = 5 * 60 * 1000; } cacheOptions.ttl = this.config.cacheTTL; this.cache = new _lruCache.default(cacheOptions); if (requiredConfigMissing) { throw new Error('must specify "uri", "db", and "collections.users" in config'); } _mongoConnector.default.setConfig(this.config); _mongoConnector.default.setLogger(this.logger); return this; } /** * Authenticate an user. * @param user user to log * @param password provided password * @param cb callback function */ async authenticate(username, password, cb) { var _this$config, _this$config2; this.logger.debug("mongodb: Authenticate user '" + username + "' with password '" + password + "'"); if (username) { var _this$cache; const user = ((_this$cache = this.cache) === null || _this$cache === void 0 ? void 0 : _this$cache.get(username)) || { password: '', groups: [] }; if ((0, _password.verifyPassword)(password, (user === null || user === void 0 ? void 0 : user.password) || '')) { // Found user with password in cache this.logger.debug(`mongodb: Found user '${username}' in cache!`); return cb(null, user === null || user === void 0 ? void 0 : user.groups); // WARN: empty group [''] evaluates to false (meaning: access denied)! // return cb(getCode(200, `Found user '${username}' in cache!`), this.cache.get(username).groups); // WARN: empty group [''] evaluates to false (meaning: access denied)! } } const client = await _mongoConnector.default.connectToDatabase((_this$config = this.config) === null || _this$config === void 0 ? void 0 : _this$config.uri); const db = await _mongoConnector.default.getDb((_this$config2 = this.config) === null || _this$config2 === void 0 ? void 0 : _this$config2.db); try { var _this$config3, _this$config4, _this$config4$fields, _this$config5, _this$config5$fields, _this$config6, _this$config6$fields, _this$config7, _this$config7$fields, _this$config8, _this$config8$fields; await client.connect(); const users = await db.collection((_this$config3 = this.config) === null || _this$config3 === void 0 ? void 0 : _this$config3.collections.users); const authQuery = `{ "${(_this$config4 = this.config) === null || _this$config4 === void 0 ? void 0 : (_this$config4$fields = _this$config4.fields) === null || _this$config4$fields === void 0 ? void 0 : _this$config4$fields.username}": "${username}" }`; const authOptions = `{ "projection": { "_id": 0, "${(_this$config5 = this.config) === null || _this$config5 === void 0 ? void 0 : (_this$config5$fields = _this$config5.fields) === null || _this$config5$fields === void 0 ? void 0 : _this$config5$fields.username}": 1, "${(_this$config6 = this.config) === null || _this$config6 === void 0 ? void 0 : (_this$config6$fields = _this$config6.fields) === null || _this$config6$fields === void 0 ? void 0 : _this$config6$fields.password}": 1, "${(_this$config7 = this.config) === null || _this$config7 === void 0 ? void 0 : (_this$config7$fields = _this$config7.fields) === null || _this$config7$fields === void 0 ? void 0 : _this$config7$fields.usergroups}": 1 } }`; const foundUsers = await users.find(JSON.parse(authQuery), JSON.parse(authOptions)); const firstUser = await foundUsers.next(); if (!firstUser || Object.keys(firstUser).length === 0 || !(0, _password.verifyPassword)(password, firstUser[(_this$config8 = this.config) === null || _this$config8 === void 0 ? void 0 : (_this$config8$fields = _this$config8.fields) === null || _this$config8$fields === void 0 ? void 0 : _this$config8$fields.password])) { cb((0, _commonsApi.getUnauthorized)(`bad username/password, access denied for username '${username}'!`), false); } else { var _this$config9, _this$config9$fields, _this$config10, _this$config10$fields, _this$config12, _this$config12$fields, _this$config13, _this$config13$fields; let groups = ['user']; if (firstUser[(_this$config9 = this.config) === null || _this$config9 === void 0 ? void 0 : (_this$config9$fields = _this$config9.fields) === null || _this$config9$fields === void 0 ? void 0 : _this$config9$fields.usergroups] && firstUser[(_this$config10 = this.config) === null || _this$config10 === void 0 ? void 0 : (_this$config10$fields = _this$config10.fields) === null || _this$config10$fields === void 0 ? void 0 : _this$config10$fields.usergroups] !== undefined) { var _this$config11, _this$config11$fields; groups = firstUser[(_this$config11 = this.config) === null || _this$config11 === void 0 ? void 0 : (_this$config11$fields = _this$config11.fields) === null || _this$config11$fields === void 0 ? void 0 : _this$config11$fields.usergroups]; } groups.push(firstUser[(_this$config12 = this.config) === null || _this$config12 === void 0 ? void 0 : (_this$config12$fields = _this$config12.fields) === null || _this$config12$fields === void 0 ? void 0 : _this$config12$fields.username]); groups = groups.filter((v, i, a) => a.indexOf(v) === i); // unique values if user was already included // Add user to cache this.cache.set(username, { password: firstUser[(_this$config13 = this.config) === null || _this$config13 === void 0 ? void 0 : (_this$config13$fields = _this$config13.fields) === null || _this$config13$fields === void 0 ? void 0 : _this$config13$fields.password], groups: groups }); this.logger.debug(`mongodb: Auth succeded for '${username}' with groups: '${JSON.stringify(groups)}'`); cb(null, groups); // WARN: empty group [''] evaluates to false (meaning: access denied)! // cb(getCode(200, `Found user '${username}' in database!`), groups); // WARN: empty group [''] evaluates to false (meaning: access denied)! } } catch (e) { this.logger.error(e); cb((0, _commonsApi.getInternalError)('error, try again: ' + e), false); } } /** * Change a user password * @param username username to create * @param password current/old password * @param newPassword new password * @param cb callback function */ changePassword(username, password, newPassword, cb) { this.logger.warn(`mongodb: changePassword called for user: ${username}`); return cb((0, _commonsApi.getInternalError)('You are not allowed to change the password via the CLI!'), false); } /** * Add a user to the database * @param username username to create * @param password provided password * @param cb callback function */ async adduser(username, password, cb) { var _this$config14, _this$config15; if (!username || username.length < 3) { return cb((0, _commonsApi.getBadData)(`Bad username, username is too short (min 3 characters)!`), false); } if (!password || password.length < 8) { return cb((0, _commonsApi.getBadData)(`Bad password, password is too short (min 8 characters)!`), false); } const client = await _mongoConnector.default.connectToDatabase((_this$config14 = this.config) === null || _this$config14 === void 0 ? void 0 : _this$config14.uri); const db = await _mongoConnector.default.getDb((_this$config15 = this.config) === null || _this$config15 === void 0 ? void 0 : _this$config15.db); try { var _this$config16; await client.connect(); const users = await db.collection((_this$config16 = this.config) === null || _this$config16 === void 0 ? void 0 : _this$config16.collections.users); if (this.config.allowAddUser) { var _this$config17, _this$config17$fields, _this$config18, _this$config18$fields; // Trying to insert user - will throw exception if duplicate username already exists const insertQuery = `{ "${(_this$config17 = this.config) === null || _this$config17 === void 0 ? void 0 : (_this$config17$fields = _this$config17.fields) === null || _this$config17$fields === void 0 ? void 0 : _this$config17$fields.username}": "${username}", "${(_this$config18 = this.config) === null || _this$config18 === void 0 ? void 0 : (_this$config18$fields = _this$config18.fields) === null || _this$config18$fields === void 0 ? void 0 : _this$config18$fields.password}": "${(0, _password.bcryptPassword)(password)}", "usergroups": ["${username}","user"] }`; const newUser = await users.insertOne(JSON.parse(insertQuery)); this.logger.info(`mongodb: Added new user: ${JSON.stringify(newUser)}`); cb(null, true); } else { this.logger.warn(`mongodb: Adding new user was disabled! You are not allowed to add users via the CLI!`); cb(null, true); // Signalling OK even if user was NOT added (or login is not possible anymore) // cb(getInternalError('You are not allowed to add users via the CLI!'), false); } } catch (e) { const error = e.message; if (error.includes('duplicate key error')) { cb(null, true); // Signalling OK even if user already exists // cb(getForbidden(`Bad username, user '${username}' already exists!`), true); } else { cb((0, _commonsApi.getInternalError)('Error with adding user to MongoDB: ' + JSON.stringify(e)), false); } } } /** * Check if user is allowed to access a package * Triggered on each access request * @param user * @param pkg * @param cb */ async allow_access(user, pkg, cb) { var _this$config$rights, _this$config$rights2; const groupsIntersection = (0, _helpers.intersect)(user.groups, (pkg === null || pkg === void 0 ? void 0 : pkg.access) || []); let hasRights = false; if (((_this$config$rights = this.config.rights) === null || _this$config$rights === void 0 ? void 0 : _this$config$rights.access) === 'maintainer') { hasRights = user.groups.includes(pkg.name); } else if (((_this$config$rights2 = this.config.rights) === null || _this$config$rights2 === void 0 ? void 0 : _this$config$rights2.access) === 'contributor') { hasRights = user.groups.includes(pkg.name); } else { var _pkg$access; hasRights = (pkg === null || pkg === void 0 ? void 0 : (_pkg$access = pkg.access) === null || _pkg$access === void 0 ? void 0 : _pkg$access.includes(user.name || '')) || groupsIntersection.length > 0; } if (hasRights || user.groups.includes(this.config.adminGroup)) { await _mongoConnector.default.incCounter('access', user.name, pkg.name, this.config); this.logger.info(`mongodb: ${user.name} has been granted access to package '${pkg.name}'`); cb(null, true); } else { var _this$config$rights3, _this$config$rights4; this.logger.error(`mongodb: ${user.name || 'anonymous user'} is not allowed to access the package '${pkg.name}' - config rights set were to '${(_this$config$rights3 = this.config.rights) === null || _this$config$rights3 === void 0 ? void 0 : _this$config$rights3.access}`); cb((0, _commonsApi.getForbidden)(`User ${user.name} is not allowed to access the package ${pkg.name} - only ${(_this$config$rights4 = this.config.rights) === null || _this$config$rights4 === void 0 ? void 0 : _this$config$rights4.access}s are!`), false); } } /** * Check if user is allowed to publish a package * Triggered on each publish request * @param user * @param pkg * @param cb */ async allow_publish(user, pkg, cb) { var _this$config$rights5, _this$config$rights6; const groupsIntersection = (0, _helpers.intersect)(user.groups, (pkg === null || pkg === void 0 ? void 0 : pkg.publish) || []); let hasRights = false; if (((_this$config$rights5 = this.config.rights) === null || _this$config$rights5 === void 0 ? void 0 : _this$config$rights5.publish) === 'maintainer') { hasRights = user.groups.includes(pkg.name); } else if (((_this$config$rights6 = this.config.rights) === null || _this$config$rights6 === void 0 ? void 0 : _this$config$rights6.publish) === 'contributor') { hasRights = user.groups.includes(pkg.name); } else { var _pkg$publish; hasRights = (pkg === null || pkg === void 0 ? void 0 : (_pkg$publish = pkg.publish) === null || _pkg$publish === void 0 ? void 0 : _pkg$publish.includes(user.name || '')) || groupsIntersection.length > 0; } if (hasRights || user.groups.includes(this.config.adminGroup)) { var _this$config$rights7; await _mongoConnector.default.incCounter('publish', user.name, pkg.name, this.config); this.logger.info(`mongodb: ${user.name} has been granted the right to publish the package '${pkg.name}' - config rights set were to '${(_this$config$rights7 = this.config.rights) === null || _this$config$rights7 === void 0 ? void 0 : _this$config$rights7.publish}'`); cb(null, true); } else { var _this$config$rights8, _this$config$rights9; this.logger.error(`mongodb: ${user.name} is not allowed to publish the package '${pkg.name}' - config rights set were to '${(_this$config$rights8 = this.config.rights) === null || _this$config$rights8 === void 0 ? void 0 : _this$config$rights8.publish}`); cb((0, _commonsApi.getForbidden)(`User ${user.name} is not allowed to publish the package ${pkg.name} - only ${(_this$config$rights9 = this.config.rights) === null || _this$config$rights9 === void 0 ? void 0 : _this$config$rights9.publish}s are!`), false); } } /** * Check if user is allowed to remove a package * Triggered on each unpublish request * @param user * @param pkg * @param cb */ async allow_unpublish(user, pkg, cb) { var _this$config$rights10, _this$config$rights11; const groupsIntersection = (0, _helpers.intersect)(user.groups, (pkg === null || pkg === void 0 ? void 0 : pkg.publish) || []); let hasRights = false; if (((_this$config$rights10 = this.config.rights) === null || _this$config$rights10 === void 0 ? void 0 : _this$config$rights10.unpublish) === 'maintainer') { hasRights = user.groups.includes(pkg.name); } else if (((_this$config$rights11 = this.config.rights) === null || _this$config$rights11 === void 0 ? void 0 : _this$config$rights11.unpublish) === 'contributor') { hasRights = user.groups.includes(pkg.name); } else { var _pkg$publish2; hasRights = (pkg === null || pkg === void 0 ? void 0 : (_pkg$publish2 = pkg.publish) === null || _pkg$publish2 === void 0 ? void 0 : _pkg$publish2.includes(user.name || '')) || groupsIntersection.length > 0; } if (hasRights || user.groups.includes(this.config.adminGroup)) { var _this$config$rights12; await _mongoConnector.default.incCounter('unpublish', user.name, pkg.name, this.config); this.logger.info(`mongodb: ${user.name} has been granted the right to unpublish the package '${pkg.name}' - config rights set were to '${(_this$config$rights12 = this.config.rights) === null || _this$config$rights12 === void 0 ? void 0 : _this$config$rights12.unpublish}`); cb(null, true); } else { var _this$config$rights13, _this$config$rights14; this.logger.error(`mongodb: ${user.name} is not allowed to unpublish the package '${pkg.name}' - config rights set were to '${(_this$config$rights13 = this.config.rights) === null || _this$config$rights13 === void 0 ? void 0 : _this$config$rights13.unpublish}`); cb((0, _commonsApi.getForbidden)(`User ${user.name} is not allowed to unpublish the package ${pkg.name} - only ${(_this$config$rights14 = this.config.rights) === null || _this$config$rights14 === void 0 ? void 0 : _this$config$rights14.unpublish}s are!`), false); } } } exports.default = AuthMongoDB;