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