UNPKG

verdaccio

Version:

A lightweight private npm proxy registry

548 lines (446 loc) 15 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _lodash = _interopRequireDefault(require("lodash")); var _constants = require("./constants"); var _pluginLoader = _interopRequireDefault(require("../lib/plugin-loader")); var _cryptoUtils = require("./crypto-utils"); var _authUtils = require("./auth-utils"); var _utils = require("./utils"); var _configUtils = require("./config-utils"); 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 LoggerApi = require('./logger'); class Auth { constructor(config) { _defineProperty(this, "config", void 0); _defineProperty(this, "logger", void 0); _defineProperty(this, "secret", void 0); _defineProperty(this, "plugins", void 0); this.config = config; this.logger = LoggerApi.logger.child({ sub: 'auth' }); this.secret = config.secret; this.plugins = this._loadPlugin(config); this._applyDefaultPlugins(); } _loadPlugin(config) { const pluginOptions = { config, logger: this.logger }; return (0, _pluginLoader.default)(config, config.auth, pluginOptions, plugin => { const { authenticate, allow_access, allow_publish } = plugin; return authenticate || allow_access || allow_publish; }); } _applyDefaultPlugins() { this.plugins.push((0, _authUtils.getDefaultPlugins)()); } changePassword(username, password, newPassword, cb) { const validPlugins = _lodash.default.filter(this.plugins, plugin => _lodash.default.isFunction(plugin.changePassword)); if (_lodash.default.isEmpty(validPlugins)) { return cb(_utils.ErrorCode.getInternalError(_constants.SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE)); } for (const plugin of validPlugins) { this.logger.trace({ username }, 'updating password for @{username}'); plugin.changePassword(username, password, newPassword, (err, profile) => { if (err) { this.logger.error({ username, err }, `An error has been produced updating the password for @{username}. Error: @{err.message}`); return cb(err); } this.logger.trace({ username }, 'updated password for @{username} was successful'); return cb(null, profile); }); } } authenticate(username, password, cb) { const plugins = this.plugins.slice(0); const self = this; (function next() { const plugin = plugins.shift(); if (_lodash.default.isFunction(plugin.authenticate) === false) { return next(); } self.logger.trace({ username }, 'authenticating @{username}'); plugin.authenticate(username, password, function (err, groups) { if (err) { self.logger.trace({ username, err }, 'authenticating for user @{username} failed. Error: @{err.message}'); return cb(err); } // Expect: SKIP if groups is falsey and not an array // with at least one item (truthy length) // Expect: CONTINUE otherwise (will error if groups is not // an array, but this is current behavior) // Caveat: STRING (if valid) will pass successfully // bug give unexpected results // Info: Cannot use `== false to check falsey values` if (!!groups && groups.length !== 0) { // TODO: create a better understanding of expectations if (_lodash.default.isString(groups)) { throw new TypeError('plugin group error: invalid type for function'); } const isGroupValid = _lodash.default.isArray(groups); if (!isGroupValid) { throw new TypeError(_constants.API_ERROR.BAD_FORMAT_USER_GROUP); } self.logger.trace({ username, groups }, 'authentication for user @{username} was successfully. Groups: @{groups}'); return cb(err, (0, _authUtils.createRemoteUser)(username, groups)); } next(); }); })(); } add_user(user, password, cb) { const self = this; const plugins = this.plugins.slice(0); this.logger.trace({ user }, 'add user @{user}'); (function next() { const plugin = plugins.shift(); let method = 'adduser'; if (_lodash.default.isFunction(plugin[method]) === false) { method = 'add_user'; } if (_lodash.default.isFunction(plugin[method]) === false) { next(); } else { // p.add_user() execution plugin[method](user, password, function (err, ok) { if (err) { self.logger.trace({ user, err }, 'the user @{user} could not being added. Error: @{err}'); return cb(err); } if (ok) { self.logger.trace({ user }, 'the user @{user} has been added'); return self.authenticate(user, password, cb); } next(); }); } })(); } /** * Allow user to access a package. */ allow_access({ packageName, packageVersion }, user, callback) { const plugins = this.plugins.slice(0); // $FlowFixMe const pkg = Object.assign({ name: packageName, version: packageVersion }, (0, _configUtils.getMatchedPackagesSpec)(packageName, this.config.packages)); const self = this; this.logger.trace({ packageName }, 'allow access for @{packageName}'); (function next() { const plugin = plugins.shift(); if (_lodash.default.isFunction(plugin.allow_access) === false) { return next(); } plugin.allow_access(user, pkg, function (err, ok) { if (err) { self.logger.trace({ packageName, err }, 'forbidden access for @{packageName}. Error: @{err.message}'); return callback(err); } if (ok) { self.logger.trace({ packageName }, 'allowed access for @{packageName}'); return callback(null, ok); } next(); // cb(null, false) causes next plugin to roll }); })(); } allow_unpublish({ packageName, packageVersion }, user, callback) { // $FlowFixMe const pkg = Object.assign({ name: packageName, version: packageVersion }, (0, _configUtils.getMatchedPackagesSpec)(packageName, this.config.packages)); this.logger.trace({ packageName }, 'allow unpublish for @{packageName}'); for (const plugin of this.plugins) { if (_lodash.default.isFunction(plugin.allow_unpublish) === false) { continue; } else { plugin.allow_unpublish(user, pkg, (err, ok) => { if (err) { this.logger.trace({ packageName }, 'forbidden publish for @{packageName}, it will fallback on unpublish permissions'); return callback(err); } if (_lodash.default.isNil(ok) === true) { this.logger.trace({ packageName }, 'we bypass unpublish for @{packageName}, publish will handle the access'); return this.allow_publish(...arguments); } if (ok) { this.logger.trace({ packageName }, 'allowed unpublish for @{packageName}'); return callback(null, ok); } }); } } } /** * Allow user to publish a package. */ allow_publish({ packageName, packageVersion }, user, callback) { const plugins = this.plugins.slice(0); const self = this; // $FlowFixMe const pkg = Object.assign({ name: packageName, version: packageVersion }, (0, _configUtils.getMatchedPackagesSpec)(packageName, this.config.packages)); this.logger.trace({ packageName }, 'allow publish for @{packageName}'); (function next() { const plugin = plugins.shift(); if (_lodash.default.isFunction(plugin.allow_publish) === false) { return next(); } plugin.allow_publish(user, pkg, (err, ok) => { if (err) { self.logger.trace({ packageName }, 'forbidden publish for @{packageName}'); return callback(err); } if (ok) { self.logger.trace({ packageName }, 'allowed publish for @{packageName}'); return callback(null, ok); } next(); // cb(null, false) causes next plugin to roll }); })(); } apiJWTmiddleware() { const plugins = this.plugins.slice(0); const helpers = { createAnonymousRemoteUser: _authUtils.createAnonymousRemoteUser, createRemoteUser: _authUtils.createRemoteUser }; for (const plugin of plugins) { if (plugin.apiJWTmiddleware) { return plugin.apiJWTmiddleware(helpers); } } return (req, res, _next) => { req.pause(); const next = function (err) { req.resume(); // uncomment this to reject users with bad auth headers // return _next.apply(null, arguments) // swallow error, user remains unauthorized // set remoteUserError to indicate that user was attempting authentication if (err) { req.remote_user.error = err.message; } return _next(); }; if (this._isRemoteUserMissing(req.remote_user)) { return next(); } // in case auth header does not exist we return anonymous function req.remote_user = (0, _authUtils.createAnonymousRemoteUser)(); const { authorization } = req.headers; if (_lodash.default.isNil(authorization)) { return next(); } if (!(0, _authUtils.isAuthHeaderValid)(authorization)) { this.logger.trace('api middleware auth heather is not valid'); return next(_utils.ErrorCode.getBadRequest(_constants.API_ERROR.BAD_AUTH_HEADER)); } const security = (0, _authUtils.getSecurity)(this.config); const { secret } = this.config; if ((0, _authUtils.isAESLegacy)(security)) { this.logger.trace('api middleware using legacy auth token'); this._handleAESMiddleware(req, security, secret, authorization, next); } else { this.logger.trace('api middleware using JWT auth token'); this._handleJWTAPIMiddleware(req, security, secret, authorization, next); } }; } _handleJWTAPIMiddleware(req, security, secret, authorization, next) { const { scheme, token } = (0, _authUtils.parseAuthTokenHeader)(authorization); if (scheme.toUpperCase() === _constants.TOKEN_BASIC.toUpperCase()) { // this should happen when client tries to login with an existing user const credentials = (0, _utils.convertPayloadToBase64)(token).toString(); const { user, password } = (0, _authUtils.parseBasicPayload)(credentials); this.authenticate(user, password, (err, user) => { if (!err) { req.remote_user = user; next(); } else { req.remote_user = (0, _authUtils.createAnonymousRemoteUser)(); next(err); } }); } else { // jwt handler const credentials = (0, _authUtils.getMiddlewareCredentials)(security, secret, authorization); if (credentials) { // if the signature is valid we rely on it req.remote_user = credentials; next(); } else { // with JWT throw 401 next(_utils.ErrorCode.getForbidden(_constants.API_ERROR.BAD_USERNAME_PASSWORD)); } } } _handleAESMiddleware(req, security, secret, authorization, next) { const credentials = (0, _authUtils.getMiddlewareCredentials)(security, secret, authorization); if (credentials) { const { user, password } = credentials; this.authenticate(user, password, (err, user) => { if (!err) { req.remote_user = user; next(); } else { req.remote_user = (0, _authUtils.createAnonymousRemoteUser)(); next(err); } }); } else { // we force npm client to ask again with basic authentication return next(_utils.ErrorCode.getBadRequest(_constants.API_ERROR.BAD_AUTH_HEADER)); } } _isRemoteUserMissing(remote_user) { return _lodash.default.isUndefined(remote_user) === false && _lodash.default.isUndefined(remote_user.name) === false; } /** * JWT middleware for WebUI */ webUIJWTmiddleware() { return (req, res, _next) => { if (this._isRemoteUserMissing(req.remote_user)) { return _next(); } req.pause(); const next = err => { req.resume(); if (err) { // req.remote_user.error = err.message; res.status(err.statusCode).send(err.message); } return _next(); }; const { authorization } = req.headers; if (_lodash.default.isNil(authorization)) { return next(); } if (!(0, _authUtils.isAuthHeaderValid)(authorization)) { return next(_utils.ErrorCode.getBadRequest(_constants.API_ERROR.BAD_AUTH_HEADER)); } const token = (authorization || '').replace(`${_constants.TOKEN_BEARER} `, ''); if (!token) { return next(); } let credentials; try { credentials = (0, _authUtils.verifyJWTPayload)(token, this.config.secret); } catch (err) {// FIXME: intended behaviour, do we want it? } if (credentials) { const { name, groups } = credentials; // $FlowFixMe req.remote_user = (0, _authUtils.createRemoteUser)(name, groups); } else { req.remote_user = (0, _authUtils.createAnonymousRemoteUser)(); } next(); }; } async jwtEncrypt(user, signOptions) { const { real_groups, name, groups } = user; const realGroupsValidated = _lodash.default.isNil(real_groups) ? [] : real_groups; const groupedGroups = _lodash.default.isNil(groups) ? real_groups : groups.concat(realGroupsValidated); const payload = { real_groups: realGroupsValidated, name, groups: groupedGroups }; const token = await (0, _cryptoUtils.signPayload)(payload, this.secret, signOptions); // $FlowFixMe return token; } /** * Encrypt a string. */ aesEncrypt(buf) { return (0, _cryptoUtils.aesEncrypt)(buf, this.secret); } } var _default = Auth; exports.default = _default;