restructure server.js, adding ldap access, part 1/2 role authorization
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Authorization
|
||||
*/
|
||||
|
||||
var common, User;
|
||||
const ldap = require ('./ldap_ohm');
|
||||
//const crypto = require ("./crypto");
|
||||
|
||||
// deactivated is not used yet
|
||||
const serverVisibleSession = { user: true, name: true, type: true, mail: true, roles: true, deactivated: true, host: true };
|
||||
const clientVisibleSession = { user: true, name: true, type: true, mail: true, roles: true };
|
||||
|
||||
|
||||
// Fill in session object
|
||||
function fillSession (req, user, roles, cb) {
|
||||
if (req.session === undefined)
|
||||
next (common.genError (500, "Error"));
|
||||
req.session.regenerate (function (err) {
|
||||
if (user !== undefined && ! err) {
|
||||
common.shallowCopy (user, serverVisibleSession, {roles: true}, req.session);
|
||||
if (user._id) {
|
||||
req.session.user = user._id;
|
||||
}
|
||||
req.session.roles = roles;
|
||||
}
|
||||
return cb (err);
|
||||
});
|
||||
}
|
||||
|
||||
const authorization = {
|
||||
// Generate Error object suitible for throwing or next()ing
|
||||
genCheckAuthorized: function (group) {
|
||||
return function (req, res, next) {
|
||||
if (req.session === undefined || req.session.user === undefined ||
|
||||
req.session.roles === undefined)
|
||||
return next (common.genError (403, "Unauthorized"));
|
||||
if (req.session.roles[group] === undefined)
|
||||
return next (common.genError (403, "Unauthorized"));
|
||||
next ();
|
||||
}
|
||||
},
|
||||
|
||||
// Login route: requires .user and .pwd params
|
||||
login: function (req, res, next) {
|
||||
var user = req.body.user || '';
|
||||
var pwd = req.body.pwd || '';
|
||||
|
||||
// Helper: Return valid session Object
|
||||
function returnSession () {
|
||||
// Only export client visible parts of session object
|
||||
var copy = common.shallowCopy (req.session, clientVisibleSession);
|
||||
return res.json (copy);
|
||||
}
|
||||
// Helper: Return error
|
||||
function returnError () {
|
||||
fillSession (req, undefined, undefined, function (err) {
|
||||
next (common.genError (401, "Unauthorized"));
|
||||
});
|
||||
}
|
||||
|
||||
// TODO Auth: validate session ID
|
||||
// Check whether to just validate current session ID
|
||||
if (user === '' && pwd === '') {
|
||||
console.log ("auth revalidate: " + req.session.user);
|
||||
if (req.session.user === undefined)
|
||||
return returnError();
|
||||
return returnSession ();
|
||||
}
|
||||
|
||||
// check local database
|
||||
User.findById (req.body.user) .exec (function (err, entry) {
|
||||
// If there is a local user AND it has a password associated, test against this, and only this
|
||||
/*
|
||||
if (entry != null && entry.pwd) {
|
||||
if (crypto.checkLocalAuth (entry, req.body.pwd)) {
|
||||
return fillSession (req, entry, common.arrayToHash(entry.roles), returnSession);
|
||||
}
|
||||
return returnError ();
|
||||
}
|
||||
*/
|
||||
|
||||
// check ldap
|
||||
ldap.authorize (user.toLowerCase(), pwd, function (found) {
|
||||
console.log ("ldap authorize " + user + " returns " + JSON.stringify (found));
|
||||
// No ldap entry either -> unauthorized
|
||||
if (found == null) {
|
||||
return returnError ();
|
||||
}
|
||||
// If there is an entry w/o password, use it for roles etc.
|
||||
if (entry) {
|
||||
if (! entry.name || entry.name === "")
|
||||
entry.name = found.name;
|
||||
if (! entry.mail || entry.mail === "")
|
||||
entry.mail = found.mail;
|
||||
if (! entry.type || entry.type === "")
|
||||
entry.type = found.type;
|
||||
if (! entry.orclgender || entry.orclgender === "")
|
||||
entry.orclgender = found.orclgender;
|
||||
return fillSession (req, entry, entry.roles.length > 0 ? common.arrayToHash(entry.roles) : {user:true}, returnSession);
|
||||
}
|
||||
// Otherwise create standard user entry
|
||||
return fillSession (req, found, {user:true}, returnSession);
|
||||
});
|
||||
});
|
||||
},
|
||||
logout: function (req, res, next) {
|
||||
fillSession (req, undefined, undefined, function (err) {
|
||||
return res.json ({});
|
||||
});
|
||||
},
|
||||
init: function (_common) {
|
||||
common = _common;
|
||||
ldap.init (_common);
|
||||
User = require('../database/user.model.js');;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
module.exports = authorization;
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Common functions and imports
|
||||
*/
|
||||
|
||||
var common = {
|
||||
fs: require('fs'), // file sync
|
||||
http: require('http'),
|
||||
mongoose: require('mongoose'), // Needed for db connection.
|
||||
//util: require('util'), // Why is it needed here?
|
||||
//fork: require('child_process') .fork, // What does that?
|
||||
|
||||
// Generate Error object suitible for throwing or next()ing
|
||||
// For a better exception handling
|
||||
genError: function (code, message) {
|
||||
var err = new Error (common.http.STATUS_CODES[code] + (message != undefined && message != "" ? ": "+message : ""));
|
||||
err.status = code;
|
||||
// to generally disable stack traces for these manually created error Objects:
|
||||
delete err.stack;
|
||||
return err;
|
||||
},
|
||||
|
||||
// Generate deep copy
|
||||
// Only include properties incl (all if undefined), strip properties excl (associative arrays)
|
||||
deepCopy: function (inp, incl, excl) {
|
||||
// For now, JSON is considered fastest / easiest
|
||||
var obj = JSON.parse (JSON.stringify (inp));
|
||||
if (incl) {
|
||||
for (var k in obj) {
|
||||
if (incl[k] === undefined)
|
||||
delete obj[k];
|
||||
}
|
||||
}
|
||||
if (excl) {
|
||||
for (var k in excl) {
|
||||
delete obj[k];
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
// Create shallow (1 level) copy of object, use obj if already present (merge)
|
||||
// Only include properties incl (all if undefined), strip properties excl (associative arrays)
|
||||
shallowCopy: function (inp, incl, excl, obj) {
|
||||
var keys = inp;
|
||||
if (obj === undefined)
|
||||
obj = {};
|
||||
if (typeof inp == "array")
|
||||
obj = [];
|
||||
if (incl !== undefined)
|
||||
keys = incl;
|
||||
for (var k in keys) {
|
||||
if (inp[k] !== undefined && (excl === undefined || ! excl[k]))
|
||||
obj[k] = inp[k];
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
// Create hash of 'true' entries for array/mongoose object
|
||||
arrayToHash: function (array) {
|
||||
var hash = {};
|
||||
for (var e=0; e < array.length; e++) {
|
||||
hash[array[e]] = true;
|
||||
}
|
||||
return hash;
|
||||
},
|
||||
|
||||
// Log output session cookie
|
||||
debug: function (req) {
|
||||
console.log ("- " + req.headers.cookie + "\n+ " + req.session.id + "\n " + JSON.stringify (req.session));
|
||||
},
|
||||
|
||||
// Init config data
|
||||
init: function () {
|
||||
this.config = JSON.parse (this.fs.readFileSync ("server_config.json"));
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = common;
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Valdiate ohm logins with ldap service
|
||||
*/
|
||||
const ldap = require('ldapjs');
|
||||
const ldap_escape = require('ldap-escape');
|
||||
|
||||
|
||||
// TODO: Where do I get the URL from?? A: Is given.
|
||||
var ldap_client = ldap.createClient({
|
||||
//url: 'ldap://gso2.ads1.fh-nuernberg.de/',
|
||||
url: 'ldap://sso.cs.ohm-hochschule.de:389/',
|
||||
//url: 'ldaps://sso.cs.ohm-hochschule.de:636/',
|
||||
reconnect: true,
|
||||
// timeouts don't work reliably
|
||||
});
|
||||
|
||||
// TODO: Where do I get the 'bindpath' parameters info from? A: Is given.
|
||||
const ldap_config = {
|
||||
bindpath: 'cn=Users,dc=ohm-hochschule,dc=de',
|
||||
timeout: 2000
|
||||
};
|
||||
|
||||
const ldap_ohm = {
|
||||
init: function () {
|
||||
},
|
||||
|
||||
// Authorize user with password
|
||||
// Calls callback with null if unauthorized
|
||||
// Calls callback with object describing user if successful:
|
||||
authorize: function (user, pwd, cb) {
|
||||
if (typeof user != 'string' || typeof pwd != 'string')
|
||||
return cb (null);
|
||||
// Empty passwords *may* bind successfully anonymously
|
||||
if (user.length < 1 || pwd.length < 1)
|
||||
return cb (null);
|
||||
|
||||
/* Same function, different writing style */
|
||||
/* Escape ldap login input */
|
||||
//escaped = ldap_escape.dn`cn=${user},`+ldap_config.bindpath;
|
||||
escaped = ldap_escape.dn (['cn=',','+ldap_config.bindpath], user);
|
||||
|
||||
// Timeout handler: call callback,
|
||||
// make sure later ldap returns don't do anything weird
|
||||
var return_object = {};
|
||||
var timeoutHandle = setTimeout (function () {
|
||||
console.log('ldap timeout');
|
||||
return_object = null;
|
||||
cb (null);
|
||||
}, ldap_config.timeout);
|
||||
|
||||
// Bind ldap to user (authorize)
|
||||
ldap_client.bind (escaped, pwd, function (err, res) {
|
||||
if (return_object === null)
|
||||
return; // Timeout, cb has already been called
|
||||
if (err !== null) {
|
||||
console.log ("ldap bind: failed for user " + user + ": " + err);
|
||||
clearTimeout (timeoutHandle);
|
||||
return cb (null);
|
||||
}
|
||||
|
||||
// Search for user entry of just bound user
|
||||
// There should be only one...
|
||||
ldap_client.search (escaped, { sizeLimit: 1 }, function (err, res) {
|
||||
if (return_object === null)
|
||||
return; // Timeout, cb has already been called
|
||||
if (err !== null) {
|
||||
console.log ("ldap search: search after bind didn't work for user "
|
||||
+ user + ": " + err);
|
||||
clearTimeout (timeoutHandle);
|
||||
return cb (null);
|
||||
}
|
||||
// Populate return with search results
|
||||
res.on('searchEntry', function(entry) {
|
||||
if (return_object === null)
|
||||
return; // Timeout, cb has already been called
|
||||
return_object.user = user;
|
||||
return_object.name = entry.object.displayname;
|
||||
return_object.type = entry.object.employeetype;
|
||||
return_object.mail = entry.object.mail;
|
||||
return_object.gender = entry.object.orclgender;
|
||||
|
||||
// Calling cb here, not in 'end', because of potential bugs with
|
||||
// concurrency failures, and we have our single(!) entry
|
||||
// https://github.com/joyent/node-ldapjs/pull/424
|
||||
clearTimeout (timeoutHandle);
|
||||
if (typeof return_object.mail != 'string' || return_object.mail.length < 1) {
|
||||
console.log("ldap search error after bind for user " + user);
|
||||
return cb (null);
|
||||
}
|
||||
return cb (return_object);
|
||||
});
|
||||
res.on('error', function(err) {
|
||||
console.log('ldap error: ' + err.message);
|
||||
});
|
||||
res.on('end', function(result) {
|
||||
// TODO: Did we forget something?
|
||||
// TODO: analyze result.status?
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ldap_ohm;
|
||||
@@ -0,0 +1,35 @@
|
||||
// Terminal call: node server/ldap_test.js - needs VPN or eduroam
|
||||
const inquirer = require('inquirer'),
|
||||
ldap = require('./ldap_ohm.js');
|
||||
|
||||
inquirer.prompt([
|
||||
{
|
||||
name: 'username',
|
||||
type: 'input',
|
||||
message: 'Enter your VirtuOhm username:',
|
||||
validate: function( value ) {
|
||||
if (value.length) {
|
||||
return true;
|
||||
} else {
|
||||
return 'Please enter your username.';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
message: 'Enter your password:',
|
||||
validate: function(value) {
|
||||
if (value.length) {
|
||||
return true;
|
||||
} else {
|
||||
return 'Please enter your password.';
|
||||
}
|
||||
}
|
||||
}])
|
||||
.then(answers => {
|
||||
ldap.init(null);
|
||||
ldap.authorize(answers.username,answers.password,function(user) {
|
||||
console.log(JSON.stringify(user));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user