diff --git a/lib/auth.js b/lib/auth.js new file mode 100644 index 000000000..92d73712d --- /dev/null +++ b/lib/auth.js @@ -0,0 +1,124 @@ +var Path = require('path') + , crypto = require('crypto') + , UError = require('./error').UError + +module.exports = Auth + +function Auth(config) { + if (!(this instanceof Auth)) return new Auth(config) + this.config = config + + if (config.users_file) { + this.HTPasswd = require('./htpasswd')( + Path.resolve( + Path.dirname(config.self_path), + config.users_file + ) + ) + } +} + +Auth.prototype.authenticate = function(user, password, cb) { + if (this.config.users != null && this.config.users[user] != null) { + // if user exists in this.users, verify password against it no matter what is in htpasswd + return cb(null, crypto.createHash('sha1').update(password).digest('hex') === this.config.users[user].password ? [user] : null) + } + + if (!this.HTPasswd) return cb(null, false) + this.HTPasswd.reload(function() { + cb(null, this.HTPasswd.verify(user, password) ? [user] : null) + }.bind(this)) +} + +Auth.prototype.add_user = function(user, password, cb) { + if (this.config.users && this.config.users[user]) return cb(new UError({ + status: 403, + message: 'this user already exists', + })) + + if (this.HTPasswd) { + if (this.max_users || this.max_users == null) { + var max_users = Number(this.max_users || Infinity) + this.HTPasswd.add_user(user, password, max_users, cb) + return + } + } + + return cb(new UError({ + status: 409, + message: 'registration is disabled', + })) +} + +Auth.prototype.middleware = function() { + var self = this + return function(req, res, _next) { + req.pause() + function next(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 (req.remote_user != null) return next() + req.remote_user = AnonymousUser() + + var authorization = req.headers.authorization + if (authorization == null) return next() + + var parts = authorization.split(' ') + + if (parts.length !== 2) return next({ + status: 400, + message: 'bad authorization header', + }) + + var scheme = parts[0] + , credentials = new Buffer(parts[1], 'base64').toString() + , index = credentials.indexOf(':') + + if (scheme !== 'Basic' || index < 0) return next({ + status: 400, + message: 'bad authorization header', + }) + + var user = credentials.slice(0, index) + , pass = credentials.slice(index + 1) + + self.authenticate(user, pass, function(err, groups) { + if (err) return next(err) + if (groups != null && groups != false) { + req.remote_user = AuthenticatedUser(user, groups) + next() + } else { + req.remote_user = AnonymousUser() + next({ + status: 403, + message: 'bad username/password, access denied', + }) + } + }) + } +} + +function AnonymousUser() { + return { + name: undefined, + // groups without '@' are going to be deprecated eventually + groups: ['@all', '@anonymous', 'all', 'undefined', 'anonymous'], + } +} + +function AuthenticatedUser(name, groups) { + groups = groups.concat(['@all', '@authenticated', 'all']) + return { + name: name, + groups: groups, + } +} + diff --git a/lib/config.js b/lib/config.js index a634e20c0..9ce4055c9 100644 --- a/lib/config.js +++ b/lib/config.js @@ -113,31 +113,23 @@ function Config(config) { if (this.ignore_latest_tag == null) this.ignore_latest_tag = false - if (this.users_file) { - this.HTPasswd = require('./htpasswd')( - Path.resolve( - Path.dirname(this.self_path), - this.users_file - ) - ) - } - return this } function allow_action(package, who, action) { return (this.get_package_setting(package, action) || []).reduce(function(prev, curr) { - if (curr === String(who) || curr === 'all') return true + if (typeof(who) === 'string' && curr === who) return true + if (Array.isArray(who) && who.indexOf(curr) !== -1) return true return prev }, false) } Config.prototype.allow_access = function(package, user) { - return allow_action.call(this, package, user, 'allow_access') || allow_action.call(this, package, user, 'access') + return allow_action.call(this, package, user.groups, 'allow_access') || allow_action.call(this, package, user, 'access') } Config.prototype.allow_publish = function(package, user) { - return allow_action.call(this, package, user, 'allow_publish') || allow_action.call(this, package, user, 'publish') + return allow_action.call(this, package, user.groups, 'allow_publish') || allow_action.call(this, package, user, 'publish') } Config.prototype.proxy_access = function(package, uplink) { @@ -145,7 +137,8 @@ Config.prototype.proxy_access = function(package, uplink) { } Config.prototype.proxy_publish = function(package, uplink) { - return allow_action.call(this, package, uplink, 'proxy_publish') + throw new Error('deprecated') + //return allow_action.call(this, package, uplink, 'proxy_publish') } Config.prototype.get_package_setting = function(package, setting) { @@ -157,38 +150,6 @@ Config.prototype.get_package_setting = function(package, setting) { return undefined } -Config.prototype.authenticate = function(user, password, cb) { - if (this.users != null && this.users[user] != null) { - // if user exists in this.users, verify password against it no matter what is in htpasswd - return cb(null, crypto.createHash('sha1').update(password).digest('hex') === this.users[user].password) - } - - if (!this.HTPasswd) return cb(null, false) - this.HTPasswd.reload(function() { - cb(null, this.HTPasswd.verify(user, password)) - }.bind(this)) -} - -Config.prototype.add_user = function(user, password, cb) { - if (this.users && this.users[user]) return cb(new UError({ - status: 403, - message: 'this user already exists', - })) - - if (this.HTPasswd) { - if (this.max_users || this.max_users == null) { - var max_users = Number(this.max_users || Infinity) - this.HTPasswd.add_user(user, password, max_users, cb) - return - } - } - - return cb(new UError({ - status: 409, - message: 'registration is disabled', - })) -} - module.exports = Config var parse_interval_table = { diff --git a/lib/index.js b/lib/index.js index 48ce0b2cf..3c83366f5 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,7 +7,6 @@ var express = require('express') , Middleware = require('./middleware') , Logger = require('./logger') , Cats = require('./status-cats') - , basic_auth = Middleware.basic_auth , validate_name = Middleware.validate_name , media = Middleware.media , expect_json = Middleware.expect_json @@ -16,6 +15,7 @@ var express = require('express') , localList = require('./local-list') , search = require('./search') , marked = require('marked') + , Auth = require('./auth') function match(regexp) { return function(req, res, next, value, name) { @@ -29,18 +29,19 @@ function match(regexp) { module.exports = function(config_hash) { var config = new Config(config_hash) - , storage = new Storage(config); + , storage = new Storage(config) + , auth = new Auth(config) search.configureStorage(storage); var can = function(action) { return function(req, res, next) { - if (config['allow_'+action](req.params.package, req.remoteUser)) { + if (config['allow_'+action](req.params.package, req.remote_user)) { next() } else { - if (!req.remoteUser) { - if (req.remoteUserError) { - var message = "can't "+action+' restricted package, ' + req.remoteUserError + if (!req.remote_user.name) { + if (req.remote_user.error) { + var message = "can't "+action+' restricted package, ' + req.remote_user.error } else { var message = "can't "+action+" restricted package without auth, did you forget 'npm set always-auth true'?" } @@ -51,7 +52,7 @@ module.exports = function(config_hash) { } else { next(new UError({ status: 403, - message: 'user '+req.remoteUser+' not allowed to '+action+' it' + message: 'user '+req.remote_user.name+' not allowed to '+action+' it' })) } } @@ -94,9 +95,7 @@ module.exports = function(config_hash) { next() }) app.use(Cats.middleware) - app.use(basic_auth(function(user, pass, cb) { - config.authenticate(user, pass, cb) - })) + app.use(auth.middleware()) app.use(express.json({strict: false, limit: config.max_body_size || '10mb'})) app.use(express.compress()) app.use(Middleware.anti_loop(config)) @@ -190,7 +189,7 @@ module.exports = function(config_hash) { storage.search(req.param.startkey || 0, {req: req}, function(err, result) { if (err) return next(err) for (var pkg in result) { - if (!config.allow_access(pkg, req.remoteUser)) { + if (!config.allow_access(pkg, req.remote_user)) { delete result[pkg] } } @@ -215,15 +214,15 @@ module.exports = function(config_hash) { app.get('/-/user/:org_couchdb_user', function(req, res, next) { res.status(200) return res.send({ - ok: 'you are authenticated as "' + req.remoteUser + '"', + ok: 'you are authenticated as "' + req.remote_user.name + '"', }) }) app.put('/-/user/:org_couchdb_user/:_rev?/:revision?', function(req, res, next) { - if (req.remoteUser != null) { + if (req.remote_user.name != null) { res.status(201) return res.send({ - ok: 'you are authenticated as "' + req.remoteUser + '"', + ok: 'you are authenticated as "' + req.remote_user.name + '"', }) } else { if (typeof(req.body.name) !== 'string' || typeof(req.body.password) !== 'string') { @@ -232,7 +231,7 @@ module.exports = function(config_hash) { message: 'user/password is not found in request (npm issue?)', })) } - config.add_user(req.body.name, req.body.password, function(err) { + auth.add_user(req.body.name, req.body.password, function(err) { if (err) { if (err.status < 500 && err.message === 'this user already exists') { // with npm registering is the same as logging in diff --git a/lib/middleware.js b/lib/middleware.js index 088db2f7b..60339a0ba 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -40,59 +40,6 @@ module.exports.expect_json = function expect_json(req, res, next) { next() } -module.exports.basic_auth = function basic_auth(callback) { - return function(req, res, _next) { - req.pause() - function next(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.remoteUserError = err.message - return _next() - } - - var authorization = req.headers.authorization - - if (req.remoteUser != null) return next() - if (authorization == null) return next() - - var parts = authorization.split(' ') - - if (parts.length !== 2) return next({ - status: 400, - message: 'bad authorization header', - }) - - var scheme = parts[0] - , credentials = new Buffer(parts[1], 'base64').toString() - , index = credentials.indexOf(':') - - if (scheme !== 'Basic' || index < 0) return next({ - status: 400, - message: 'bad authorization header', - }) - - var user = credentials.slice(0, index) - , pass = credentials.slice(index + 1) - - callback(user, pass, function(err, is_ok) { - if (err) return next(err) - if (is_ok) { - req.remoteUser = user - next() - } else { - next({ - status: 403, - message: 'bad username/password, access denied', - }) - } - }) - } -} - module.exports.anti_loop = function(config) { return function(req, res, next) { if (req.headers.via != null) { @@ -187,7 +134,7 @@ module.exports.log_and_etagify = function(req, res, next) { req.log.warn({ request: {method: req.method, url: req.url}, level: 35, // http - user: req.remoteUser, + user: req.remote_user.name, status: res.statusCode, error: res._sinopia_error, bytes: {