diff --git a/src/lib/auth-utils.js b/src/lib/auth-utils.js new file mode 100644 index 000000000..b546857b2 --- /dev/null +++ b/src/lib/auth-utils.js @@ -0,0 +1,39 @@ +import {ErrorCode} from './utils'; +import {API_ERROR} from './constants'; + +export function allow_action(action) { + return function(user, pkg, cb) { + const ok = pkg[action].reduce(function(prev, curr) { + if (user.name === curr || user.groups.indexOf(curr) !== -1) { + return true; + } + + return prev; + }, false); + + if (ok) { + return cb(null, true); + } + + if (user.name) { + cb(ErrorCode.getForbidden(`user ${user.name} is not allowed to ${action} package ${pkg.name}`)); + } else { + cb(ErrorCode.getForbidden(`unregistered users are not allowed to ${action} package ${pkg.name}`)); + } + }; +} + +export function getDefaultPlugins() { + return { + authenticate: function(user, password, cb) { + cb(ErrorCode.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD)); + }, + + add_user: function(user, password, cb) { + return cb(ErrorCode.getConflict(API_ERROR.BAD_USERNAME_PASSWORD)); + }, + + allow_access: allow_action('access'), + allow_publish: allow_action('publish'), + }; +} diff --git a/src/lib/auth.js b/src/lib/auth.js index ee4b80a8e..e74e5a63a 100644 --- a/src/lib/auth.js +++ b/src/lib/auth.js @@ -2,21 +2,20 @@ import _ from 'lodash'; import {loadPlugin} from '../lib/plugin-loader'; -import {ErrorCode} from './utils'; +import {buildBase64Buffer, ErrorCode} from './utils'; import {aesDecrypt, aesEncrypt, signPayload, verifyPayload} from './crypto-utils'; import type {Config, Logger, Callback} from '@verdaccio/types'; import type {$Response, NextFunction} from 'express'; import type {$RequestExtend, JWTPayload} from '../../types'; -import {API_ERROR, ROLES} from './constants'; +import {API_ERROR, HTTP_STATUS, ROLES, TOKEN_BASIC, TOKEN_BEARER} from './constants'; import {getMatchedPackagesSpec} from './config-utils'; - +import type {IAuth} from '../../types'; +import {getDefaultPlugins} from './auth-utils'; const LoggerApi = require('./logger'); -/** - * Handles the authentification, load auth plugins. - */ -class Auth { + +class Auth implements IAuth { config: Config; logger: Logger; secret: string; @@ -32,48 +31,18 @@ class Auth { } _loadPlugin(config: Config) { - const plugin_params = { + const pluginOptions = { config, logger: this.logger, }; - return loadPlugin(config, config.auth, plugin_params, function(p) { - return p.authenticate || p.allow_access || p.allow_publish; + return loadPlugin(config, config.auth, pluginOptions, function(plugin) { + return plugin.authenticate || plugin.allow_access || plugin.allow_publish; }); } _applyDefaultPlugins() { - const allow_action = function(action) { - return function(user, pkg, cb) { - const ok = pkg[action].reduce(function(prev, curr) { - if (user.name === curr || user.groups.indexOf(curr) !== -1) return true; - return prev; - }, false); - - if (ok) { - return cb(null, true); - } - - if (user.name) { - cb(ErrorCode.getForbidden(`user ${user.name} is not allowed to ${action} package ${pkg.name}`)); - } else { - cb(ErrorCode.getForbidden(`unregistered users are not allowed to ${action} package ${pkg.name}`)); - } - }; - }; - - this.plugins.push({ - authenticate: function(user, password, cb) { - cb(ErrorCode.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD)); - }, - - add_user: function(user, password, cb) { - return cb(ErrorCode.getConflict(API_ERROR.BAD_USERNAME_PASSWORD)); - }, - - allow_access: allow_action('access'), - allow_publish: allow_action('publish'), - }); + this.plugins.push(getDefaultPlugins()); } authenticate(user: string, password: string, cb: Callback) { @@ -81,7 +50,7 @@ class Auth { (function next() { const plugin = plugins.shift(); - if (typeof(plugin.authenticate) !== 'function') { + if (_.isFunction(plugin.authenticate) === false) { return next(); } @@ -99,12 +68,12 @@ class Auth { // Info: Cannot use `== false to check falsey values` if (!!groups && groups.length !== 0) { // TODO: create a better understanding of expectations - if (typeof groups === 'string') { + if (_.isString(groups)) { throw new TypeError('invalid type for function'); } const isGroupValid: boolean = _.isArray(groups); if (!isGroupValid) { - throw new TypeError('user groups is different than an array'); + throw new TypeError(API_ERROR.BAD_FORMAT_USER_GROUP); } return cb(err, authenticatedUser(user, groups)); @@ -116,19 +85,19 @@ class Auth { add_user(user: string, password: string, cb: Callback) { let self = this; - let plugins = this.plugins.slice(0) + let plugins = this.plugins.slice(0); - ;(function next() { - let p = plugins.shift(); - let n = 'adduser'; - if (typeof(p[n]) !== 'function') { - n = 'add_user'; + (function next() { + let plugin = plugins.shift(); + let method = 'adduser'; + if (_.isFunction(plugin[method]) === false) { + method = 'add_user'; } - if (typeof(p[n]) !== 'function') { + if (_.isFunction[method] === false) { next(); } else { // p.add_user() execution - p[n](user, password, function(err, ok) { + plugin[method](user, password, function(err, ok) { if (err) { return cb(err); } @@ -185,7 +154,7 @@ class Auth { return next(); } - plugin.allow_publish(user, pkg, function(err, ok) { + plugin.allow_publish(user, pkg, (err, ok) => { if (err) { return callback(err); } @@ -220,13 +189,13 @@ class Auth { req.remote_user = buildAnonymousUser(); const authorization = req.headers.authorization; - if (authorization == null) { + if (_.isNil(authorization)) { return next(); } const parts = authorization.split(' '); if (parts.length !== 2) { - return next( ErrorCode.getBadRequest('bad authorization header') ); + return next( ErrorCode.getBadRequest(API_ERROR.BAD_AUTH_HEADER) ); } const credentials = this._parseCredentials(parts); @@ -258,12 +227,12 @@ class Auth { _parseCredentials(parts: Array) { let credentials; const scheme = parts[0]; - if (scheme.toUpperCase() === 'BASIC') { - credentials = new Buffer(parts[1], 'base64').toString(); - this.logger.info('basic authentication is deprecated, please use JWT instead'); + if (scheme.toUpperCase() === TOKEN_BASIC.toUpperCase()) { + credentials = buildBase64Buffer(parts[1]).toString(); + this.logger.info(API_ERROR.DEPRECATED_BASIC_HEADER); return credentials; - } else if (scheme.toUpperCase() === 'BEARER') { - const token = new Buffer(parts[1], 'base64'); + } else if (scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) { + const token = buildBase64Buffer(parts[1]); credentials = aesDecrypt(token, this.secret).toString('utf8'); return credentials; @@ -277,17 +246,17 @@ class Auth { */ webUIJWTmiddleware() { return (req: $RequestExtend, res: $Response, _next: NextFunction) => { - if (req.remote_user !== null && req.remote_user.name !== undefined) { + if (_.isNull(req.remote_user) === false && _.isNil(req.remote_user.name) === false) { return _next(); } req.pause(); - const next = function(_err) { + const next = () => { req.resume(); return _next(); }; - const token = (req.headers.authorization || '').replace('Bearer ', ''); + const token = (req.headers.authorization || '').replace(`${TOKEN_BEARER} `, ''); if (!token) { return next(); } @@ -329,7 +298,7 @@ class Auth { try { decoded = verifyPayload(token, this.secret); } catch (err) { - throw ErrorCode.getCode(401, err.message); + throw ErrorCode.getCode(HTTP_STATUS.UNAUTHORIZED, err.message); } return decoded; @@ -351,7 +320,7 @@ function buildAnonymousUser() { return { name: undefined, // groups without '$' are going to be deprecated eventually - groups: ['$all', '$anonymous', '@all', '@anonymous'], + groups: [ROLES.$ALL, ROLES.$ANONYMOUS, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_ANONUMOUS], real_groups: [], }; } @@ -362,7 +331,12 @@ function buildAnonymousUser() { */ function authenticatedUser(name: string, pluginGroups: Array) { const isGroupValid: boolean = _.isArray(pluginGroups); - const groups = (isGroupValid ? pluginGroups : []).concat([ROLES.$ALL, ROLES.$AUTH, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_AUTH, ROLES.ALL]); + const groups = (isGroupValid ? pluginGroups : []).concat([ + ROLES.$ALL, + ROLES.$AUTH, + ROLES.DEPRECATED_ALL, + ROLES.DEPRECATED_AUTH, + ROLES.ALL]); return { name, diff --git a/src/lib/config-utils.js b/src/lib/config-utils.js index e560552e2..7c1923616 100644 --- a/src/lib/config-utils.js +++ b/src/lib/config-utils.js @@ -7,6 +7,7 @@ import {ErrorCode} from './utils'; import type {PackageList} from '@verdaccio/types'; import type {MatchedPackage} from '../../types'; + const BLACKLIST = { all: true, anonymous: true, diff --git a/src/lib/constants.js b/src/lib/constants.js index 0a813a5fc..79f3b6ff3 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -29,10 +29,12 @@ export const DEFAULT_UPLINK = 'npmjs'; export const ROLES = { $ALL: '$all', + ALL: 'all', $AUTH: '$authenticated', + $ANONYMOUS: '$anonymous', DEPRECATED_ALL: '@all', DEPRECATED_AUTH: '@authenticated', - ALL: 'all', + DEPRECATED_ANONUMOUS: '@anonymous', }; export const HTTP_STATUS = { @@ -80,7 +82,10 @@ export const API_ERROR = { FILE_NOT_FOUND: 'File not found', BAD_STATUS_CODE: 'bad status code', PACKAGE_EXIST: 'this package is already present', + BAD_AUTH_HEADER: 'bad authorization header', WEB_DISABLED: 'Web interface is disabled in the config file', + DEPRECATED_BASIC_HEADER: 'basic authentication is deprecated, please use JWT instead', + BAD_FORMAT_USER_GROUP: 'user groups is different than an array', }; export const APP_ERROR = { diff --git a/src/lib/utils.js b/src/lib/utils.js index e0e42a064..2b70b0837 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -29,6 +29,10 @@ export function getUserAgent(): string { return `${pkgName}/${pkgVersion}`; } +export function buildBase64Buffer(payload: string): Buffer { + return new Buffer(payload, 'base64'); +} + /** * Validate a package. * @return {Boolean} whether the package is valid or not