Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for custom BDs by user's roles #120

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,19 @@ SuperLogin also allows you to specify default `_security` roles for members and

CouchDB can save your API a lot of traffic by handling both reads and writes. CouchDB provides the [validate_doc_update function](http://guide.couchdb.org/draft/validation.html) to approve or disapprove what gets written. However, since your CouchDB users are temporary random API keys, you have no idea which user is requesting to write. SuperLogin has inserted the original `user_id` into `userCtx.roles[0]`, prefixed by `user:` (e.g. `user:superman`).

Example design doc:
``` js
module.exports = {
validator: {
validate_doc_update: function (newDoc, oldDoc, userCtx) {
if (!newDoc.name) {
throw({forbidden: 'doc.name is required'});
}
}.toString()
}
};
```

If you are using Cloudant authentication, the prefixed `user_id` is inserted as the first item on the `permissions` array, which will also appear inside `roles` in your `userCtx` object. You will also find all the `roles` from your user doc here.

If you wish to give a user special Cloudant permissions other than the ones specified in your config, you can edit the user doc from the `sl-users` database and under `personalDBs` add an array called `permissions` under the corresponding DB for that user.
Expand Down Expand Up @@ -261,7 +274,7 @@ It's easy to add custom fields to user documents. When added to a `profile` fiel
2. Include the fields with [registrations](#post-register).
3. To also fill in custom fields after social authentications use the [superlogin.onCreate](#superloginoncreatefn) handler. Example:

``` js
``` js
superlogin.onCreate(function(userDoc, provider) {
if(userDoc.profile === undefined) {
userDoc.profile = {};
Expand Down
9 changes: 6 additions & 3 deletions config.example.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,14 @@ module.exports = {
confirmEmailRedirectURL: '/',
// Set this to true to disable usernames and use emails instead
emailUsername: false,
// If this is more then zero a random ID of that length will be used in place of the username. This number must
// be even and must be big enough to allow it to be unique. I would suggest at least 16.
randomUIDLength: 0,
// Custom names for the username and password fields in your sign-in form
usernameField: 'user',
passwordField: 'pass',
// Override default constraints
passwordConstraints = {
passwordConstraints: {
length: {
minimum: 6,
message: "must be at least 6 characters"
Expand Down Expand Up @@ -130,7 +133,7 @@ module.exports = {
},
// These are settings for each personal database
model: {
// If your database is not listed below, these default settings will be applied
// If your database is not listed below, these default settings will be applied
_default: {
// Array containing name of the design doc files (omitting .js extension), in the directory configured below
designDocs: ['mydesign'],
Expand Down Expand Up @@ -191,4 +194,4 @@ module.exports = {
}
}
}
};
};
9 changes: 7 additions & 2 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,14 @@ gulp.task('user-test', ['dbauth-test'], function () {
.pipe(mocha({timeout: 2000}));
});

gulp.task('customdbs-test', ['user-test'], function () {
return gulp.src(['test/customdbs.spec.js'], {read: false})
.pipe(mocha({timeout: 2000}));
});

gulp.task('final-test', ['user-test'], function () {
return gulp.src(['test/test.js'], {read: false})
.pipe(mocha({timeout: 2000}));
.pipe(mocha({timeout: 2000}));
});

gulp.task('default', ['final-test', 'user-test', 'mailer-test', 'session-test', 'middleware-test', 'lint']);
gulp.task('default', ['final-test','customdbs-test','user-test', 'mailer-test', 'session-test', 'middleware-test', 'lint']);
100 changes: 60 additions & 40 deletions lib/dbauth/cloudant.js
Original file line number Diff line number Diff line change
@@ -1,126 +1,146 @@
'use strict';
var url = require('url');
var urlParse = require('url-parse');
var BPromise = require('bluebird');
var request = require('superagent');
var util = require('./../util');

// This is not needed with Cloudant
exports.storeKey = function() {
exports.storeKey = function () {
return BPromise.resolve();
};

// This is not needed with Cloudant
exports.removeKeys = function() {
exports.removeKeys = function () {
return BPromise.resolve();
};

// This is not needed with Cloudant
exports.initSecurity = function() {
exports.initSecurity = function () {
return BPromise.resolve();
};

exports.authorizeKeys = function(user_id, db, keys, permissions, roles) {
exports.authorizeKeys = function (user_id, db, keys, permissions, roles) {
var keysObj = {};
if(!permissions) {
if (!permissions) {
permissions = ['_reader', '_replicator'];
}
permissions = permissions.concat(roles || []);
permissions.unshift('user:' + user_id);
// If keys is a single value convert it to an Array
keys = util.toArray(keys);
// Check if keys is an array and convert it to an object
if(keys instanceof Array) {
keys.forEach(function(key) {
if (keys instanceof Array) {
keys.forEach(function (key) {
keysObj[key] = permissions;
});
} else {
keysObj = keys;
}
// Pull the current _security doc
return getSecurityCloudant(db)
.then(function(secDoc) {
if(!secDoc._id) {
.then(function (secDoc) {
if (!secDoc._id) {
secDoc._id = '_security';
}
if(!secDoc.cloudant) {
if (!secDoc.cloudant) {
secDoc.cloudant = {};
}
Object.keys(keysObj).forEach(function(key) {
Object.keys(keysObj).forEach(function (key) {
secDoc.cloudant[key] = keysObj[key];
});
return putSecurityCloudant(db, secDoc);
});
};

exports.deauthorizeKeys = function(db, keys) {
exports.deauthorizeKeys = function (db, keys) {
// cast keys to an Array
keys = util.toArray(keys);
return getSecurityCloudant(db)
.then(function(secDoc) {
.then(function (secDoc) {
var changes = false;
if(!secDoc.cloudant) {
if (!secDoc.cloudant) {
return BPromise.resolve(false);
}
keys.forEach(function(key) {
if(secDoc.cloudant[key]) {
keys.forEach(function (key) {
if (secDoc.cloudant[key]) {
changes = true;
delete secDoc.cloudant[key];
}
});
if(changes) {
if (changes) {
return putSecurityCloudant(db, secDoc);
} else {
return BPromise.resolve(false);
}
});
};

exports.getAPIKey = function(db) {
var parsedUrl = url.parse(db.getUrl());
exports.getAPIKey = function (db) {
var parsedUrl = url.parse(getBaseUrl(db));
parsedUrl.pathname = '/_api/v2/api_keys';
var finalUrl = url.format(parsedUrl);
return BPromise.fromNode(function(callback) {
request.post(finalUrl)
.set(db.getHeaders())
.end(callback);
})
.then(function(res) {
return BPromise.fromNode(function (callback) {
request.post(finalUrl)
.set(db.getHeaders())
.end(callback);
})
.then(function (res) {
var result = JSON.parse(res.text);
if(result.key && result.password && result.ok === true) {
if (result.key && result.password && result.ok === true) {
return BPromise.resolve(result);
} else {
return BPromise.reject(result);
}
}, function (err) {
console.log("Error getAPIKey(" + finalUrl + "): " + JSON.stringify(err));
});
};

var getSecurityCloudant = exports.getSecurityCloudant = function (db) {
var finalUrl = getSecurityUrl(db);
return BPromise.fromNode(function(callback) {
request.get(finalUrl)
.set(db.getHeaders())
.end(callback);
})
.then(function(res) {
return BPromise.fromNode(function (callback) {
request.get(finalUrl)
.set(db.getHeaders())
.end(callback);
})
.then(function (res) {
return BPromise.resolve(JSON.parse(res.text));
}, function (err) {
console.log("Error getSecurityCloudant(" + finalUrl + "): " + JSON.stringify(err));
});
};

var putSecurityCloudant = exports.putSecurityCloudant = function (db, doc) {
var finalUrl = getSecurityUrl(db);
return BPromise.fromNode(function(callback) {
request.put(finalUrl)
.set(db.getHeaders())
.send(doc)
.end(callback);
})
.then(function(res) {
return BPromise.fromNode(function (callback) {
request.put(finalUrl)
.set(db.getHeaders())
.send(doc)
.end(callback);
})
.then(function (res) {
return BPromise.resolve(JSON.parse(res.text));
}, function (err) {
console.log("Error putSecurityCloudant(" + finalUrl + "): " + JSON.stringify(err));
});
};

function getSecurityUrl(db) {
var parsedUrl = url.parse(db.getUrl());
var parsedUrl = url.parse(getBaseUrl(db));
parsedUrl.pathname = parsedUrl.pathname + '_security';
return url.format(parsedUrl);
}

function getBaseUrl(db) {
if (typeof db.getUrl === 'function') { // pouchdb pre-6.0.0
// console.log("db.getUrl() = " + JSON.stringify(db.getUrl()));
return db.getUrl();
} else if (db.__opts && db.__opts.prefix) { // PouchDB.defaults
// console.log("db.__opts.prefix = " + JSON.stringify(db.__opts.prefix));
return db.__opts.prefix;
} else { // pouchdb post-6.0.0
// console.log("urlParse(db.name(" + db.name + ")).origin = " + JSON.stringify(urlParse(db.name).origin));
return urlParse(db.name).origin;
}
}
Loading