mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-01-06 22:40:26 -05:00
auth refactoring, part 1
moving stuff to auth.js basically + allowing usergroups
This commit is contained in:
parent
7742e11cde
commit
8086c6f0bf
4 changed files with 145 additions and 114 deletions
124
lib/auth.js
Normal file
124
lib/auth.js
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
29
lib/index.js
29
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
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in a new issue