Merge remote-tracking branch 'XTUPLE/master' into 20864-grid
[xtuple] / node-datasource / routes / auth.js
1 /*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
2 regexp:true, undef:true, strict:true, trailing:true, white:true */
3 /*global X:true, _:true, SYS:true */
4
5 (function () {
6   "use strict";
7
8   /**
9     @name Auth
10     @class Auth
11     */
12
13   var passport = require('passport'),
14       url = require('url');
15
16   /**
17     Receives user authentication credentials and have passport do the authentication.
18    */
19   exports.login = [
20     //passport.authenticate('local', { successReturnToOrRedirect: '/login/scope', failureRedirect: '/', failureFlash: 'Invalid username or password.' }),
21     passport.authenticate('local', { failureRedirect: '/?login=fail' }),
22     function (req, res, next) {
23
24       if (req && req.session && !req.session.oauth2 && req.session.passport && req.session.passport.user && req.session.passport.user.organization) {
25         res.redirect("/" + req.session.passport.user.organization + '/app');
26         //next();
27       } else {
28         exports.scopeForm(req, res, next);
29       }
30     }
31   ];
32
33   /**
34     Renders the login form
35    */
36   exports.loginForm = function (req, res) {
37     var message = [];
38
39     if (req.query && req.query.login && req.query.login === 'fail') {
40       message = ["Invalid username or password."];
41     }
42
43     res.render('login', { message: message, databases: X.options.datasource.databases });
44   };
45
46   /**
47     Logs out user by removing the session and sending the user to the login screen.
48    */
49   exports.logout = function (req, res) {
50     if (req.session.passport) {
51       // Make extra sure passport is empty.
52       req.session.passport = null;
53     }
54
55     if (req.session) {
56       // Kill the whole session, db, cache and all.
57       req.session.destroy(function () {});
58     }
59
60     if (req.path.split("/")[1]) {
61       res.clearCookie(req.path.split("/")[1] + ".sid");
62     }
63
64     req.logout();
65     res.redirect('/');
66   };
67
68   /**
69     Receives a request telling us which organization a user has selected
70     to log into. Note that we don't trust the client; we check
71     to make sure that the user actually belongs to that organization.
72    */
73   exports.scope = function (req, res, next) {
74     var userId = req.session.passport.user.id,
75       selectedOrg = req.body.org,
76       user = new SYS.User(),
77       options = {};
78
79     options.success = function (response) {
80       var privs,
81           userOrg,
82           userName;
83
84       if (response.length === 0) {
85         if (req.session && req.session.oauth2 && req.session.oauth2.redirectURI) {
86           X.log("OAuth 2.0 User %@ has no business trying to log in to organization %@.".f(userId, selectedOrg));
87           res.redirect(req.session.oauth2.redirectURI + '?error=access_denied');
88           return;
89         }
90
91         X.log("User %@ has no business trying to log in to organization %@.".f(userId, selectedOrg));
92         res.redirect('/' + selectedOrg + '/logout');
93         return;
94       } else if (response.length > 1) {
95         X.log("More than one User: %@ exists.".f(userId));
96         res.redirect('/' + selectedOrg + '/logout');
97         return;
98       }
99
100       // We can now trust this user's request to log in to this organization.
101
102       // Update the session store row to add the org choice and username.
103       // Note: Updating this object magically persists the data into the SessionStore table.
104
105       //privs = _.map(response.get("privileges"), function (privAss) {
106       //  return privAss.privilege.name;
107       //});
108
109       //_.each(response.get('organizations'), function (orgValue, orgKey, orgList) {
110       //  if (orgValue.name === selectedOrg) {
111       //    userOrg = orgValue.name;
112       //    userName = orgValue.username;
113       //  }
114       //});
115
116       //if (!userOrg || !userName) {
117       if (!response.get("username")) {
118         // This shouldn't happen.
119         X.log("User %@ has no business trying to log in to organization %@.".f(userId, selectedOrg));
120         res.redirect('/' + selectedOrg + '/logout');
121         return;
122       }
123
124       //req.session.passport.user.globalPrivileges = privs;
125       req.session.passport.user.organization = response.get("organization");
126       req.session.passport.user.username = response.get("username");
127
128 // TODO - req.oauth probably isn't enough here, but it's working 2013-03-15...
129       // If this is an OAuth 2.0 login with only 1 org.
130       if (req.oauth2) {
131         return next();
132       }
133
134       // If this is an OAuth 2.0 login with more than 1 org.
135       if (req.session.returnTo) {
136         res.redirect(req.session.returnTo);
137       } else {
138         // Redirect to start loading the client app.
139         res.redirect('/client');
140       }
141     };
142
143     options.error = function (model, error) {
144       X.log("userorg fetch error", error);
145       res.redirect('/' + selectedOrg + '/logout');
146       return;
147     };
148
149
150     // The user id we're searching for.
151     options.id = userId;
152
153     // The user under whose authority the query is run.
154     options.username = X.options.databaseServer.user;
155     options.database = selectedOrg;
156
157     // Verify that the org is valid for the user.
158     user.fetch(options);
159   };
160
161   /**
162     Loads the form to let the user choose their organization. If there's only one
163     organization for the user we choose for them.
164    */
165   exports.scopeForm = function (req, res, next) {
166     var organizations = [],
167         scope,
168         scopes = [];
169
170     try {
171       organizations = _.map(req.user.get("organizations"), function (org) {
172         return org.name;
173       });
174     } catch (error) {
175       // Prevent unauthorized access.
176       res.redirect('/');
177       return;
178     }
179
180     // If this is an OAuth 2.0 login req, try and get the org from the requested scope.
181     if (req.session && req.session.oauth2) {
182
183       if (req.session.oauth2.req && req.session.oauth2.req.scope && req.session.oauth2.req.scope.length > 0) {
184         // Loop through the scope URIs and convert them to org names.
185         _.each(req.session.oauth2.req.scope, function (value, key, list) {
186           var org;
187
188           // Get the org from the scope URI e.g. 'dev' from: 'https://mobile.xtuple.com/auth/dev'
189           scope = url.parse(value, true);
190           org = scope.path.split("/")[1] || null;
191
192           // TODO - Still need more work to support userinfo calls.
193           // See node-datasource/oauth2/oauth2.js authorization.
194
195           // The scope 'https://mobile.xtuple.com/auth/userinfo.xxx' can be used to make userinfo
196           // REST calls and is not a valid org scope, we'll skip it here.
197           if (org && org.indexOf('userinfo') === -1) {
198             scopes[key] = org;
199           }
200         });
201
202         // If we only have one scope/org sent, choose it for this request.
203         if (scopes.length === 1) {
204           req.body.org = scopes[0];
205           exports.scope(req, res, next);
206           return;
207         }
208
209         // TODO - Multiple scopes sent.
210         // Do we want to let them select an org or respond with error and scopeList?
211         // It depends on the scenario. Some support user interaction and can select an org, others
212         // do not and should get an error.
213
214       }
215
216       // TODO - No scope is sent.
217       // Do we want to let them select an org or respond with error and scopeList?
218       // It depends on the scenario. Some support user interaction and can select an org, others
219       // do not and should get an error.
220
221     }
222
223     // Below will handle OAuth "TODO - Multiple scopes sent", "TODO - No scope is sent." above for now.
224
225     // Choose an org automatically if there's only one for this user.
226     if (organizations.length === 1) {
227       req.body.org = organizations[0];
228       exports.scope(req, res, next);
229       return;
230     }
231
232     // Some users may not have any orgs. They should not get this far.
233     if (organizations.length === 0) {
234       X.err("User: %@ shall not pass, they have no orgs to select.".f(req.session.passport.user.id));
235       req.flash('orgerror', 'You have not been assigned to any organizations.');
236     }
237
238     // We've got nothing, let the user choose their scope/org.
239     res.render('scope', { organizations: organizations.sort(), message: req.flash('orgerror') });
240   };
241 }());