blob: 874e021864e48f42717f44ccfc0eebba60153671 [file] [log] [blame]
// sync to $host/_users/_design/_auth
var ddoc = {_id:"_design/_auth", language:"javascript"}
module.exports = ddoc
ddoc.lists = {
index: function (head,req) {
var row
, out = {}
, id, data
while (row = getRow()) {
id = row.id.replace(/^org\.couchdb\.user:/, '')
data = row.value
delete data._id
delete data._rev
delete data.salt
delete data.password_sha
delete data.type
delete data.roles
delete data._deleted_conflicts
out[id] = data
}
send(toJSON(out))
},
email:function (head, req) {
var row
, data
, id
, email = req.query.email || undefined
, out = []
while (row = getRow()) {
id = row.id.replace(/^org\.couchdb\.user:/, '')
data = row.value
var dm = data.email || undefined
if (data.email !== email) continue
out.push(row.value.name)
}
send(toJSON(out))
}
}
ddoc.validate_doc_update = function (newDoc, oldDoc, userCtx, secObj) {
if (newDoc._deleted === true) {
// allow deletes by admins
if ((userCtx.roles.indexOf('_admin') !== -1)) {
return;
} else {
throw({forbidden: 'Only admins may delete user docs.'});
}
}
if ((oldDoc && oldDoc.type !== 'user') || newDoc.type !== 'user') {
throw({forbidden : 'doc.type must be user'});
} // we only allow user docs for now
if (!newDoc.name) {
throw({forbidden: 'doc.name is required'});
}
if (newDoc.roles && !isArray(newDoc.roles)) {
throw({forbidden: 'doc.roles must be an array'});
}
if (newDoc._id !== ('org.couchdb.user:' + newDoc.name)) {
throw({
forbidden: 'Doc ID must be of the form org.couchdb.user:name'
});
}
if (newDoc.name !== newDoc.name.toLowerCase()) {
throw({
forbidden: 'Name must be lower-case'
})
}
if (newDoc.name !== encodeURIComponent(newDoc.name)) {
throw({
forbidden: 'Name cannot contain non-url-safe characters'
})
}
if (newDoc.name.charAt(0) === '.') {
throw({
forbidden: 'Name cannot start with .'
})
}
if (!(newDoc.email && newDoc.email.match(/^.+@.+\..+$/))) {
throw({forbidden: 'Email must be an email address'})
}
if (oldDoc) { // validate all updates
if (oldDoc.name !== newDoc.name) {
throw({forbidden: 'Usernames can not be changed.'});
}
}
if (newDoc.password_sha && !newDoc.salt) {
throw({
forbidden: 'Users with password_sha must have a salt.'
});
}
var is_server_or_database_admin = function(userCtx, secObj) {
// see if the user is a server admin
if(userCtx.roles.indexOf('_admin') !== -1) {
return true; // a server admin
}
// see if the user a database admin specified by name
if(secObj && secObj.admins && secObj.admins.names) {
if(secObj.admins.names.indexOf(userCtx.name) !== -1) {
return true; // database admin
}
}
// see if the user a database admin specified by role
if(secObj && secObj.admins && secObj.admins.roles) {
var db_roles = secObj.admins.roles;
for(var idx = 0; idx < userCtx.roles.length; idx++) {
var user_role = userCtx.roles[idx];
if(db_roles.indexOf(user_role) !== -1) {
return true; // role matches!
}
}
}
return false; // default to no admin
}
if (!is_server_or_database_admin(userCtx, secObj)) {
if (oldDoc) { // validate non-admin updates
if (userCtx.name !== newDoc.name) {
throw({
forbidden: 'You may only update your own user document.'
});
}
if (oldDoc.email !== newDoc.email) {
throw({
forbidden: 'You may not change your email address\n' +
'Please visit https://npmjs.org/email-edit to do so.'
})
}
// validate role updates
var oldRoles = oldDoc.roles.sort();
var newRoles = newDoc.roles.sort();
if (oldRoles.length !== newRoles.length) {
throw({forbidden: 'Only _admin may edit roles'});
}
for (var i = 0; i < oldRoles.length; i++) {
if (oldRoles[i] !== newRoles[i]) {
throw({forbidden: 'Only _admin may edit roles'});
}
}
} else if (newDoc.roles.length > 0) {
throw({forbidden: 'Only _admin may set roles'});
}
}
// no system roles in users db
for (var i = 0; i < newDoc.roles.length; i++) {
if (newDoc.roles[i][0] === '_') {
throw({
forbidden: 'No system roles (starting with underscore) in users db.'
});
}
}
// no system names as names
if (newDoc.name[0] === '_') {
throw({forbidden: 'Username may not start with underscore.'});
}
var badUserNameChars = [':'];
for (var i = 0; i < badUserNameChars.length; i++) {
if (newDoc.name.indexOf(badUserNameChars[i]) >= 0) {
throw({forbidden: 'Character `' + badUserNameChars[i] +
'` is not allowed in usernames.'});
}
}
}
ddoc.views = {
listAll: { map : function (doc) { return emit(doc._id, doc) } },
invalidUser: { map: function (doc) {
var errors = []
if (doc.type !== 'user') {
errors.push('doc.type must be user')
}
if (!doc.name) {
errors.push('doc.name is required')
}
if (doc.roles && !isArray(doc.roles)) {
errors.push('doc.roles must be an array')
}
if (doc._id !== ('org.couchdb.user:' + doc.name)) {
errors.push('Doc ID must be of the form org.couchdb.user:name')
}
if (doc.name !== doc.name.toLowerCase()) {
errors.push('Name must be lower-case')
}
if (doc.name !== encodeURIComponent(doc.name)) {
errors.push('Name cannot contain non-url-safe characters')
}
if (doc.name.charAt(0) === '.') {
errors.push('Name cannot start with .')
}
if (!(doc.email && doc.email.match(/^.+@.+\..+$/))) {
errors.push('Email must be an email address')
}
if (doc.password_sha && !doc.salt) {
errors.push('Users with password_sha must have a salt.')
}
if (!errors.length) return
emit([doc.name, doc.email], errors)
}},
invalid: { map: function (doc) {
if (doc.type !== 'user') {
return emit(['doc.type must be user', doc.email, doc.name], 1)
}
if (!doc.name) {
return emit(['doc.name is required', doc.email, doc.name], 1)
}
if (doc.roles && !isArray(doc.roles)) {
return emit(['doc.roles must be an array', doc.email, doc.name], 1)
}
if (doc._id !== ('org.couchdb.user:' + doc.name)) {
return emit(['Doc ID must be of the form org.couchdb.user:name', doc.email, doc.name], 1)
}
if (doc.name !== doc.name.toLowerCase()) {
return emit(['Name must be lower-case', doc.email, doc.name], 1)
}
if (doc.name !== encodeURIComponent(doc.name)) {
return emit(['Name cannot contain non-url-safe characters', doc.email, doc.name], 1)
}
if (doc.name.charAt(0) === '.') {
return emit(['Name cannot start with .', doc.email, doc.name], 1)
}
if (!(doc.email && doc.email.match(/^.+@.+\..+$/))) {
return emit(['Email must be an email address', doc.email, doc.name], 1)
}
if (doc.password_sha && !doc.salt) {
return emit(['Users with password_sha must have a salt.', doc.email, doc.name], 1)
}
}, reduce: '_sum'}
}
if (require.main === module) {
console.log(JSON.stringify(ddoc, function (k, v) {
if (typeof v !== 'function') return v;
return v.toString()
}))
}