1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
4 require_once 'Exception/Authorization.php';
8 require_once 'Auth.php';
10 /* Each object in the system has an identifier, like 'ticket:XYZ' that
11 * indicates the type of object as well as its own identifier.
13 * Each object may also have a discressionary access control list (DACL) that
14 * describes what actions members of particular roles are permitted to
15 * the object. The DACL can apply either to the object itself, or be
16 * a cascading (or inherited) entry that applies only to objects that are
17 * children of the object in question.
19 * When determining whether access is permitted, the DACL is walked from
20 * the object being accessed up to the root. As soon as the allow/deny
21 * status us known for a specific (role, action) combination, the search
24 * DACL entries can be explicitly ordered so that a particular user from
25 * a group can be excepted from a blanket allow/deny rule that follows.
30 static $cache = array();
31 static $genealogist = array();
33 static public function addRootObjectAndRoles($name)
35 /* construct some roles that encapsulate read, modify, write */
36 $rolebase = preg_replace('/s$/', '', $name);
39 array("{$rolebase}Viewer", "read", true),
40 array("{$rolebase}Editor", "read", true),
41 array("{$rolebase}Editor", "modify", true),
42 array("{$rolebase}Creator", "read", true),
43 array("{$rolebase}Creator", "modify", true),
44 array("{$rolebase}Creator", "create", true),
45 array("{$rolebase}Creator", "delete", true),
47 MTrackACL::setACL($name, true, $ents);
49 array("{$rolebase}Viewer", "read", true),
50 array("{$rolebase}Editor", "read", true),
51 array("{$rolebase}Creator", "read", true),
52 array("{$rolebase}Creator", "modify", true),
53 array("{$rolebase}Creator", "create", true),
54 array("{$rolebase}Creator", "delete", true),
56 MTrackACL::setACL($name, false, $ents);
59 /* functions that we can call to determine ancestry */
60 static public function registerAncestry($objtype, $func)
62 self::$genealogist[$objtype] = $func;
65 /* returns the objectid path that leads from the root to the specified
66 * object, including the object itself as the last element */
67 static public function getParentPath($objectid, $steps = -1)
70 while (strlen($objectid)) {
71 if ($steps != -1 && $steps-- == 0) {
75 if (isset(self::$genealogist[$objectid])) {
76 $func = self::$genealogist[$objectid];
77 if (is_string($func)) {
80 $parent = call_user_func($func, $objectid);
86 if (preg_match("/^(.*):([^:]+)$/", $objectid, $M)) {
88 if (isset(self::$genealogist[$class])) {
89 $func = self::$genealogist[$class];
90 if (is_string($func)) {
93 $parent = call_user_func($func, $objectid);
107 /* computes the overall ACL as it applies to someone that belongs to the
108 * indicated set of roles. */
109 static public function computeACL($objectid, $role_list)
111 $key = $objectid . '~' . join('~', $role_list);
113 if (isset(self::$cache[$key])) {
114 return self::$cache[$key];
117 /* we calculate the path to the object from its parent, and pull
118 * out all ACL entries on those objects that match the provided
119 * role list, ordering from the object up to the root.
123 $db = MTrackDB::get();
124 foreach ($role_list as $r => $rname) {
125 $rlist[] = $db->quote($r);
127 // Always want the special wildcard 'everybody' entry
128 $rlist[] = $db->quote('*');
129 $role_list = join(',', $rlist);
134 $path = self::getParentPath($objectid);
135 foreach ($path as $oid) {
136 $oidlist[] = $db->quote($oid);
138 $oidlist = join(',', $oidlist);
141 select objectid as id, action, cascade, allow
146 and objectid in ($oidlist)
155 # Collect the results and index by objectid; we'll need to walk over
157 $res_by_oid = array();
159 foreach (MTrackDB::q($sql)->fetchAll(PDO::FETCH_ASSOC) as $row) {
160 $res_by_oid[$row['id']][] = $row;
162 foreach ($path as $oid) {
163 if (!isset($res_by_oid[$oid])) continue;
164 foreach ($res_by_oid[$oid] as $row) {
166 if ($row['id'] == $objectid && $row['cascade']) {
167 /* ignore items below the object of interest */
171 if (!isset($actions[$row['action']])) {
172 $actions[$row['action']] = $row['allow'] ? true : false;
177 self::$cache[$key] = $actions;
182 /* Entries is an array of [role, action, allow] tuples in the order
183 * that they should be checked.
184 * If cascade is true, then these entries will replace the
185 * inheritable set, otherwise they will replace the entries
187 * If entries is an empty array, or not an array, then the appropriate
188 * ACL will be removed.
190 static public function setACL($object, $cascade, $entries)
192 self::$cache = array();
194 $cascade = (int)$cascade;
195 MTrackDB::q('delete from acl where objectid = ? and cascade = ?',
198 if (is_array($entries)) {
199 foreach ($entries as $ent) {
200 if (isset($ent['role'])) {
201 $role = $ent['role'];
202 $action = $ent['action'];
203 $allow = $ent['allow'];
205 list($role, $action, $allow) = $ent;
207 MTrackDB::q('insert into acl (objectid, cascade, seq, role,
208 action, allow) values (?, ?, ?, ?, ?, ?)',
209 $object, $cascade, $seq++,
210 $role, $action, (int)$allow);
215 /* Obtains the ACL entries for the specified object.
216 * If cascade is true, it will return the inheritable ACL.
218 static public function getACL($object, $cascade)
220 return MTrackDB::q('select role, action, allow from acl
221 where objectid = ? and cascade = ? order by seq',
222 $object, (int)$cascade)->fetchAll(PDO::FETCH_ASSOC);
225 static public function hasAllRights($object, $rights)
227 if (MTrackAuth::getUserClass() == 'admin') {
230 if (!is_array($rights)) {
231 $rights = array($rights);
233 if (!count($rights)) {
234 throw new Exception("can't have all of no rights");
236 $acl = self::computeACL($object, MTrackAuth::getGroups());
237 # echo "ACL: $object<br>";
243 foreach ($rights as $action) {
244 if (!isset($acl[$action]) || !$acl[$action]) {
251 static public function hasAnyRights($object, $rights)
253 if (MTrackAuth::getUserClass() == 'admin') {
256 if (!is_array($rights)) {
257 $rights = array($rights);
259 if (!count($rights)) {
260 throw new Exception("can't have any of no rights");
262 $acl = self::computeACL($object, MTrackAuth::getGroups());
265 foreach ($rights as $action) {
266 if (isset($acl[$action]) && $acl[$action]) {
273 static public function requireAnyRights($object, $rights)
275 if (!self::hasAnyRights($object, $rights)) {
276 require_once 'Exception/Authorization.php';
277 throw new MTrackAuthorizationException("Not authorized", $rights);
281 static public function requireAllRights($object, $rights)
283 if (!self::hasAllRights($object, $rights)) {
284 require_once 'Exception/Authorization.php';
285 throw new MTrackAuthorizationException("Not authorized", $rights);
289 /* helper for generating an ACL editor.
290 * As parameters, takes an objectid indicating the object being edited,
291 * and an action map which breaks down tasks into groups.
292 * Each group consists of a set of permissions, starting with the least
293 * permissive in that group, through to most permissive.
294 * Each group will be rendered as a select box, and a synthetic "none"
295 * entry will be generated for the group that explicitly excludes each
296 * of the other permission levels in that group.
298 * The form element that is generated will contain a JSON representation
299 * of an "ents" array that can be passed to setACL().
301 static public function renderACLForm($formprefix, $objectid, $map)
303 $ident = preg_replace("/[^a-zA-Z]/", '', $formprefix);
305 $groups = MTrackAuth::enumGroups();
307 foreach (MTrackDB::q('select userid, fullname from userinfo where active = 1')
308 ->fetchAll() as $row) {
309 if (isset($groups[$row[0]])) continue;
310 if (strlen($row[1])) {
311 $disp = "$row[0] - $row[1]";
315 $groups[$row[0]] = $disp;
317 if (!isset($groups['*'])) {
318 $groups['*'] = '(Everybody)';
321 // Encode the map into an object
322 $mobj = new stdClass;
327 foreach ($map as $group => $actions) {
328 // Each subsequent action in a group implies access greater than
329 // the item that preceeds it
331 $all_perms = array_keys($actions);
333 foreach ($all_perms as $p) {
334 $prohibit[$p] = "-$p";
336 $none = join('|', $prohibit);
338 $a[] = array($none, 'None');
341 foreach ($actions as $perm => $label) {
343 unset($prohibit[$perm]);
344 $p = join('|', array_merge($accum, $prohibit));
345 $a[] = array($p, $label);
346 /* save this for reverse engineering the right group in the current
348 $reng[$perm] = $group;
349 $rank[$group][$perm] = $i++;
351 $mobj->{$group} = $a;
353 $mobj = json_encode($mobj);
355 $roledefs = new stdclass;
356 $acl = self::getACL($objectid, 0);
357 foreach ($acl as $ent) {
358 $group = $reng[$ent['action']];
359 $act = ($ent['allow'] ? '' : '-') . $ent['action'];
360 $roledefs->{$ent['role']}->{$group}[] = $act;
362 if (!isset($groups[$ent['role']])) {
363 $groups[$ent['role']] = $ent['role'];
366 $roledefs = json_encode($roledefs);
368 /* let's figure out the inherited ACL */
369 $path = self::getParentPath($objectid, 2);
370 $inherited = new stdclass;
371 if (count($path) == 2) {
372 $pacl = self::getACL($path[1], 1);
373 foreach ($pacl as $ent) {
374 // Not relevant per the specified action map
375 if (!isset($reng[$ent['action']])) continue;
377 $group = $reng[$ent['action']];
378 $act = ($ent['allow'] ? '' : '-') . $ent['action'];
379 $inherited->{$ent['role']}->{$group}[] = $act;
381 if (!isset($groups[$ent['role']])) {
382 $groups[$ent['role']] = $ent['role'];
386 // Inheritable set may not be specified directly in
387 // the same terms as the action_map, so we need to infer it
388 // Example: we may have read|modify leaving delete unspecified.
389 // We treat this as read|modify|-delete
390 foreach ($inherited as $role => $agroups) {
391 foreach ($agroups as $group => $actions) {
393 foreach ($actions as $act) {
394 if ($act[0] == '-') continue;
395 if ($highest === null || $rank[$group][$act] > $highest) {
396 $highest = $rank[$group][$act];
400 if ($highest === null) {
401 unset($inherited->{$role}->{$group});
404 // Compute full value
406 foreach ($rank[$group] as $act => $i) {
407 if ($i <= $highest) {
413 $inherited->{$role}->{$group} = join('|', $comp);
417 $inherited = json_encode($inherited);
421 $groups = json_encode($groups);
422 $cat_order = json_encode(array_keys($map));
425 <div class='permissioneditor'>
430 <em>Select "Add" to define permissions for an entity.
431 The first matching permission is taken as definitive,
432 so if a given user belongs to multiple groups and matches
433 multiple permission rows, the first is taken. You may
434 drag to re-order permissions.
438 <em>Permissions inherited from the parent of this object are
439 shown as non-editable entries at the top of the list. You may
440 override them by adding your own explicit entry.</em>
443 <input type='hidden' id='$formprefix' name='$formprefix'>
444 <table id='acl$ident'>
453 $(document).ready(function () {
454 var cat_order = $cat_order;
455 var groups = $groups;
456 var roledefs = $roledefs;
457 var inherited = $inherited;
459 var disp = $('#acl$ident');
460 var tbody = $('tbody', disp);
462 var field = $('#$formprefix');
464 function add_acl_entity(role)
466 // Delete role from select box
467 $('option', sel).each(function () {
468 if ($(this).attr('value') == role) {
472 // Create a row for this role
473 var sp = $('<tr style="cursor:pointer"/>');
476 .html('<span style="position: absolute; margin-left: -1.3em" class="ui-icon ui-icon-arrowthick-2-n-s"></span>')
477 .append(groups[role])
481 for (var gi in cat_order) {
482 var group = cat_order[gi];
483 var gsel = $('<select/>');
484 gsel.data('acl.role', role);
485 var data = mobj[group];
486 for (var i in data) {
494 if (roledefs[role]) {
495 gsel.val(roledefs[role][group].join('|'));
502 var b = $('<button>x</button>');
507 b.click(function () {
517 var tr = $('thead tr', disp);
518 // Add columns for action groups
519 for (var gi in cat_order) {
520 var group = cat_order[gi];
521 tr.append($('<th/>').text(group));
523 // Add fixed inherited rows
524 var thead = $('thead', disp);
525 for (var role in inherited) {
526 tr = $('<tr class="inheritedacl"/>');
527 tr.append($('<td/>').text(groups[role]));
528 for (var group in mobj) {
529 var d = inherited[role][group];
531 // Good old fashioned look up (we don't have this hashed)
532 for (var i in mobj[group]) {
533 var ent = mobj[group][i];
539 tr.append($('<td/>').text(d));
541 tr.append($('<td>(Not Specified)</td>'));
546 sel = $('<select/>');
552 for (var i in groups) {
561 /* make the tbody sortable. Note that we append the "Add..." to the table,
562 * not the tbody, so that we don't allow dragging it around */
565 for (var role in roledefs) {
566 add_acl_entity(role);
569 sel.change(function () {
576 field.parents('form:first').submit(function () {
578 $('select', tbody).each(function () {
579 var role = $(this).data('acl.role');
580 var val = $(this).val().split('|');
584 if (action.substring(0, 1) == '-') {
586 action = action.substring(1);
588 acl.push([role, action, allow]);
591 field.val(JSON.stringify(acl));