verdaccio-mongodb
Version:
MongoDB Authentication plugin for Verdaccio
404 lines (312 loc) • 21 kB
JavaScript
"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;