import
[web.mtrack] / inc / auth.php
1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
3
4 include_once MTRACK_INC_DIR . '/auth/http.php';
5 include_once MTRACK_INC_DIR . '/auth/openid.php';
6
7 interface IMTrackAuth {
8   /** Returns the authenticated user, or null if authentication is
9    * required */
10   function authenticate();
11
12   /** Called if the user is not authenticated as a registered
13    * user and if the page requires it.
14    * Should initiate whatever is appropriate to begin the authentication
15    * process (eg: displaying logon information).
16    * You may assume that no output has been sent to the client at
17    * the time that this function is called.
18    * Returns null if not supported, throw an exception if failed,
19    * else return a the authenticated user (if it can be determined
20    * by the time the function returns).
21    * If an alternate login page is displayed, this function should
22    * exit instead of returning.
23    */
24   function doAuthenticate($force = false);
25
26   /** Returns a list of available groups.
27    * Returns null if not supported, throw an exception if failed. */
28   function enumGroups();
29
30   /** Returns a list of groups that a given user belongs to.
31    * Returns null if not supported, throw an exception if failed. */
32   function getGroups($username);
33
34   /** Adds a user to a group.
35    * Returns null if not supported, throw an exception if failed,
36    * return true if succeeded */
37   function addToGroup($username, $groupname);
38
39   /** Removes a user from a group.
40    * Returns null if not supported, throw an exception if failed,
41    * return true if succeeded */
42   function removeFromGroup($username, $groupname);
43
44   /** Returns userdata for a given user id
45    * Some authentication mechanisms outsource the storage of user data.
46    * This function returns null if no additional information is available,
47    * or an array containing the following keys:
48    *   email - the email address
49    *   fullname - the full name
50    *   avatar - URL to an avatar image
51    */
52   function getUserData($username);
53 }
54
55 class MTrackAuth
56 {
57   static $stack = array();
58   static $mechs = array();
59   static $group_assoc = array();
60
61   public static function registerMech(IMTrackAuth $mech) {
62     self::$mechs[] = $mech;
63   }
64
65   /** switch user */
66   public static function su($user) {
67     if (!strlen($user)) throw new Exception("invalid user");
68     array_unshift(self::$stack, $user);
69   }
70
71   /** returns the instance of an auth mechanism given its class name */
72   public static function getMech($name) {
73     foreach (self::$mechs as $inst) {
74       if ($inst instanceof $name) {
75         return $inst;
76       }
77     }
78     return null;
79   }
80
81   /** drop identity set by last su */
82   public static function drop() {
83     if (count(self::$stack) == 0) {
84       throw new Exception("no privs to drop");
85     }
86     return array_shift(self::$stack);
87   }
88
89   /** returns the authenticated user, or null if authentication
90    * is required */
91   public static function authenticate() {
92     foreach (self::$mechs as $mech) {
93       $name = $mech->authenticate();
94       if ($name !== null) {
95         return $name;
96       }
97     }
98
99     /* always fall back on the unix username when running from
100      * the console */
101     if (php_sapi_name() == 'cli') {
102       static $envs = array('MTRACK_LOGNAME', 'LOGNAME', 'USER');
103       foreach ($envs as $name) {
104         if (isset($_ENV[$name])) {
105           return $_ENV[$name];
106         }
107       }
108     } elseif (count(self::$mechs) == 0 &&
109         MTrackConfig::get('core', 'admin_party') == 1
110         && ($_SERVER['REMOTE_ADDR'] == '127.0.0.1' ||
111           $_SERVER['REMOTE_ADDR'] == '::1')) {
112       return 'adminparty';
113     }
114
115     return null;
116   }
117
118   public static function isAuthConfigured() {
119     return count(self::$mechs) ? true : false;
120   }
121
122   /** determine the current identity.  If doauth is true (default),
123    * then the authentication hook will be invoked */
124   public static function whoami($doauth = true) {
125     if (count(self::$stack) == 0 && $doauth) {
126       try {
127         $who = self::authenticate();
128         if ($who === null) {
129           foreach (self::$mechs as $mech) {
130             $who = $mech->doAuthenticate();
131             if ($who !== null) {
132               break;
133             }
134           }
135         }
136         if ($who !== null) {
137           self::su($who);
138         }
139       } catch (Exception $e) {
140         if (php_sapi_name() != 'cli') {
141           header('HTTP/1.0 401 Unauthorized');
142           echo "<h1>Not authorized</h1>";
143           echo htmlentities($e->getMessage());
144         } else {
145           echo " ** Not authorized\n\n";
146           echo $e->getMessage() . "\n";
147         }
148         error_log($e->getMessage());
149         exit(1);
150       }
151     }
152     if (!count(self::$stack)) {
153       return "anonymous";
154     }
155     return self::$stack[0];
156   }
157
158   static function getUserClass($user = null) {
159     if ($user === null) {
160       $user = self::whoami();
161     }
162     if (MTrackConfig::get('core', 'admin_party') == 1
163         && $user == 'adminparty'
164         && ($_SERVER['REMOTE_ADDR'] == '127.0.0.1' ||
165           $_SERVER['REMOTE_ADDR'] == '::1')) {
166       return 'admin';
167     }
168
169     $user_class = MTrackConfig::get('user_classes', $user);
170     if ($user_class === null) {
171       if ($user == 'anonymous') {
172         return 'anonymous';
173       }
174       return 'authenticated';
175     }
176     return $user_class;
177   }
178
179   static $userdata_cache = array();
180   static function getUserData($username) {
181     $username = mtrack_canon_username($username);
182
183     if (array_key_exists($username, self::$userdata_cache)) {
184       return self::$userdata_cache[$username];
185     }
186     $data = null;
187     foreach (self::$mechs as $mech) {
188       $data = $mech->getUserData($username);
189       if ($data !== null) {
190         break;
191       }
192     }
193     if ($data === null) {
194       foreach (MTrackDB::q(
195           'select fullname, email from userinfo where userid = ?',
196           $username)->fetchAll(PDO::FETCH_ASSOC) as $row) {
197         $data = $row;
198         break;
199       }
200     }
201     if ($data === null) {
202       $data = array(
203         'fullname' => $username
204       );
205     }
206
207     if (!isset($data['email'])) {
208       if (preg_match('/<([a-z0-9_.+=-]+@[a-z0-9.-]+)>/', $username, $M)) {
209         // username contains an email address
210         $data['email'] = $M[1];
211       } else if (preg_match('/^([a-z0-9_.+=-]+@[a-z0-9.-]+)$/', $username)) {
212         // username is an email address
213         $data['email'] = $username;
214       } else if (preg_match('/^[a-z0-9_.+=-]+$/', $username)) {
215         // valid localpart; assume a domain and construct an email address
216         $dom = MTrackConfig::get('core', 'default_email_domain');
217         if ($dom !== null) {
218           $data['email'] = $username . '@' . $dom;
219         }
220       }
221     }
222
223     self::$userdata_cache[$username] = $data;
224
225     return $data;
226   }
227
228   /* enumerates possible groups from the auth plugin layer */
229   static function enumGroups() {
230     $groups = array();
231     foreach (self::$mechs as $mech) {
232       $g = $mech->enumGroups();
233       if (is_array($g)) {
234         foreach ($g as $i => $grp) {
235           if (is_integer($i)) {
236             $groups[$grp] = $grp;
237           } else {
238             $groups[$i] = $grp;
239           }
240         }
241       }
242     }
243     /* merge in our project groups */
244     foreach (MTrackDB::q('select project, g.name, p.name from groups g left join projects p on g.project = p.projid')
245         as $row) {
246       $gid = "project:$row[0]:$row[1]";
247       $groups[$gid] = "$row[1] ($row[2])";
248     }
249     return $groups;
250   }
251
252   /* returns groups of which the authenticated user is a member */
253   static function getGroups($user = null) {
254     if ($user === null) {
255       $user = self::whoami();
256     }
257     $canon = mtrack_canon_username($user);
258
259     if (isset(self::$group_assoc[$user])) {
260       return self::$group_assoc[$user];
261     }
262
263     $roles = array($canon => $canon);
264
265     $user_class = self::getUserClass($user); // FIXME: $canon?
266     $class_roles = MTrackConfig::get('user_class_roles', $user_class);
267     foreach (preg_split('/\s*,\s*/', $class_roles) as $role) {
268       $roles[$role] = $role;
269     }
270
271     foreach (self::$mechs as $mech) {
272       $g = $mech->getGroups($user);
273       if (is_array($g)) {
274         foreach ($g as $i => $grp) {
275           if (is_integer($i)) {
276             $roles[$grp] = $grp;
277           } else {
278             $roles[$i] = $grp;
279           }
280         }
281       }
282     }
283     /* merge in our project group membership */
284     foreach (MTrackDB::q('select project, groupname, p.name from group_membership gm left join projects p on gm.project = p.projid where username = ?',
285         $canon)->fetchAll() as $row) {
286       $gid = "project:$row[0]:$row[1]";
287       $roles[$gid] = "$row[1] ($row[2])";
288     }
289
290     self::$group_assoc[$user] = $roles;
291     return $roles;
292   }
293
294   static function forceAuthenticate() {
295     try {
296       $who = self::authenticate();
297       if ($who === null) {
298         foreach (self::$mechs as $mech) {
299           $who = $mech->doAuthenticate(true);
300           if ($who !== null) {
301             break;
302           }
303         }
304       }
305       if ($who !== null) {
306         self::su($who);
307       }
308     } catch (Exception $e) {
309     }
310   }
311 }
312