accesslib: get_user_by_capability() - Handle the complex case

With this commit, we can handle the complex cases with

 - correct pagination, but not very efficient over large datasets

 - mostly-correct application of the override rules

The structure of the code is fairly complex in that we want to do
it without holding all the recs in memory, so we use a small state
machine. We have to handle the complex override rules over 1 or 2
permissions (when $doanything is set) so it all ends up quite complex.

There is one known issue with this code, in cases where the default
role ends up as the decider between 2 conflicting RAs, we fail to
apply it. This will need a bit of reorg of how the loop works.

MDL-12452
This commit is contained in:
martinlanghoff 2008-01-06 23:23:58 +00:00
parent 2d1669b0d5
commit a4436c6db8

View file

@ -4165,11 +4165,15 @@ function get_default_course_role($course) {
/** /**
* who has this capability in this context * Who has this capability in this context?
* does not handling user level resolving!!! *
* (!)pleaes note if $fields is empty this function attempts to get u.* * This can be a very expensive call - use sparingly and keep
* which can get rather large. * the results if you are going to need them again soon.
* i.e 1 person has 2 roles 1 allow, 1 prevent, this will not work properly *
* Note if $fields is empty this function attempts to get u.*
* which can get rather large - and has a serious perf impact
* on some DBs.
*
* @param $context - object * @param $context - object
* @param $capability - string capability * @param $capability - string capability
* @param $fields - fields to be pulled * @param $fields - fields to be pulled
@ -4238,6 +4242,14 @@ function get_users_by_capability($context, $capability, $fields='', $sort='',
return false; return false;
} }
// is the default role interesting? does it have
// a relevant rolecap? (we use this a lot later)
if (in_array((int)$CFG->defaultuserroleid, $roleids, true)) {
$defaultroleinteresting = true;
} else {
$defaultroleinteresting = false;
}
// //
// Prepare query clauses // Prepare query clauses
// //
@ -4321,7 +4333,7 @@ function get_users_by_capability($context, $capability, $fields='', $sort='',
} }
// all site users have it, anyway // all site users have it, anyway
if (in_array((int)$CFG->defaultuserroleid, $roleids, true)) { if ($defaultroleinteresting) {
$sql = "SELECT $fields $sql = "SELECT $fields
FROM {$CFG->prefix}user u FROM {$CFG->prefix}user u
$uljoin $uljoin
@ -4351,12 +4363,16 @@ function get_users_by_capability($context, $capability, $fields='', $sort='',
// //
// If there are any negative rolecaps, we need to // If there are any negative rolecaps, we need to
// work through a subselect - which is a lot more complex, // work through a subselect that will bring several rows
// and we cannot do the job in pure SQL (not without SQL stored // per user (one per RA).
// procedures anyway). We will have to select with multiple-rows // Since we cannot do the job in pure SQL (not without SQL stored
// and apply $limitfrom/$limitnum on the PHP side. // procedures anyway), we end up tied to processing the data in PHP
// all the way down to pagination.
// //
// Did I say yuck? // In some cases, this will mean bringing across a ton of data --
// when paginating, we have to walk the permisisons of all the rows
// in the _previous_ pages to get the pagination correct in the case
// of users that end up not having the permission - this removed.
// //
// Prepare the role permissions datastructure for fast lookups // Prepare the role permissions datastructure for fast lookups
@ -4365,29 +4381,29 @@ function get_users_by_capability($context, $capability, $fields='', $sort='',
$rid = (int)$rc->roleid; $rid = (int)$rc->roleid;
$perm = (int)$rc->permission; $perm = (int)$rc->permission;
$depth = (int)$rc->ctxdepth; $rcdepth = (int)$rc->ctxdepth;
if (!isset($roleperms[$rid])) { if (!isset($roleperms[$rc->capability][$rid])) {
$roleperms[$rid] = (object)array('perm' => $perm, $roleperms[$rc->capability][$rid] = (object)array('perm' => $perm,
'depth' => $depth); 'rcdepth' => $rcdepth);
} else { } else {
if ($roleperms[$rid]->perm == CAP_PROHIBIT) { if ($roleperms[$rc->capability][$rid]->perm == CAP_PROHIBIT) {
continue; continue;
} }
// override - as we are going // override - as we are going
// from general to local perms // from general to local perms
// (as per the ORDER BY...depth ASC above) // (as per the ORDER BY...depth ASC above)
// and local perms win... // and local perms win...
$roleperms[$rid] = (object)array('perm' => $perm, $roleperms[$rc->capability][$rid] = (object)array('perm' => $perm,
'depth' => $depth); 'rcdepth' => $rcdepth);
} }
} }
if ($context->contextlevel == CONTEXT_SYSTEM if ($context->contextlevel == CONTEXT_SYSTEM
|| $isfrontpage || $isfrontpage
|| in_array((int)$CFG->defaultuserroleid, $roleids, true)) { || $defaultroleinteresting) {
// Handle system / sitecourse / defaultcap-with-neg-overrides // Handle system / sitecourse / defaultrole-with-perhaps-neg-overrides
// with a SELECT FROM user LEFT OUTER JOIN against ra - // with a SELECT FROM user LEFT OUTER JOIN against ra -
// This is expensive on the SQL and PHP sides - // This is expensive on the SQL and PHP sides -
// moves a ton of data across the wire. // moves a ton of data across the wire.
@ -4396,15 +4412,15 @@ function get_users_by_capability($context, $capability, $fields='', $sort='',
$ss = "SELECT u.id as userid, ra.roleid, $ss = "SELECT u.id as userid, ra.roleid,
ctx.depth ctx.depth
FROM {$CFG->prefix}user u FROM {$CFG->prefix}user u
LEFT OUTER {$CFG->prefix}role_assignments ra LEFT OUTER JOIN {$CFG->prefix}role_assignments ra
ON (ra.userid = u.id ON (ra.userid = u.id
AND ra.contextid IN ($ctxids) AND ra.contextid IN ($ctxids)
AND ra.roleid IN (".implode(',',$roleids) .") AND ra.roleid IN (".implode(',',$roleids) ."))
LEFT OUTER JOIN {$CFG->prefix}context ctx LEFT OUTER JOIN {$CFG->prefix}context ctx
ON ra.contextid=ctx.id ON ra.contextid=ctx.id
WHERE u.deleted=0"; WHERE u.deleted=0";
} else { } else {
// "Normal" case - the rolecaps we are after will // "Normal complex case" - the rolecaps we are after will
// be defined in a role assignment somewhere. // be defined in a role assignment somewhere.
$ss = "SELECT ra.userid as userid, ra.roleid, $ss = "SELECT ra.userid as userid, ra.roleid,
ctx.depth ctx.depth
@ -4425,7 +4441,7 @@ function get_users_by_capability($context, $capability, $fields='', $sort='',
$where .= ' AND ' . implode(' AND ', array_values($wherecond)); $where .= ' AND ' . implode(' AND ', array_values($wherecond));
} }
// Each user's entries should come clustered together // Each user's entries MUST come clustered together
// and RAs ordered in depth DESC - the role/cap resolution // and RAs ordered in depth DESC - the role/cap resolution
// code depends on this. // code depends on this.
$sort .= ' , ra.userid ASC, ra.depth DESC'; $sort .= ' , ra.userid ASC, ra.depth DESC';
@ -4433,92 +4449,193 @@ function get_users_by_capability($context, $capability, $fields='', $sort='',
$rs = get_recordset_sql($select.$from.$where.$sortby); $rs = get_recordset_sql($select.$from.$where.$sortby);
// Process the user accounts, folding repeats together... //
$users = array(); // Process the user accounts+RAs, folding repeats together...
$lastuserid = 0; //
// The processing for this recordset is tricky - to fold
// the role/perms of users with multiple role-assignments
// correctly while still processing one-row-at-a-time
// we need to add a few additional 'private' fields to
// the results array - so we can treat the rows as a
// state machine to track the cap/perms and at what RA-depth
// and RC-depth they were defined.
//
$results = array();
// pagination controls
$c = 0; $c = 0;
$limitfrom = (int)$limitfrom; $limitfrom = (int)$limitfrom;
$limitnum = (int)$limitnum; $limitnum = (int)$limitnum;
// What caps we are tracking
$caps = array($capability);
if ($doanything) {
$caps[] = 'moodle/site:candoanything';
}
//
// Mini-state machine, using $lastuserid and $hascap
// $hascap[ 'moodle/foo:bar' ]->perm = CAP_SOMETHING (numeric constant)
// $hascap[ 'moodle/foo:bar' ]->radepth = depth of the role assignment that set it
// $hascap[ 'moodle/foo:bar' ]->rcdepth = depth of the rolecap that set it
// -- when resolving conflicts, we need to look into radepth first, if unresolved
//
$lastuserid = 0;
foreach ($caps as $cap) {
$hascap[$cap]->perm = 0; // the main cap we are lookin
$hascap[$cap]->radepth = 0;
$hascap[$cap]->rcdepth = 0;
}
unset($cap);
while ($user = rs_fetch_next_record($rs)) { while ($user = rs_fetch_next_record($rs)) {
//error_log(" Record: " . print_r($user,1));
//
// Pagination controls // Pagination controls
// Note that we might end up removing a user
// that ends up _not_ having the rights,
// therefore rolling back $c
//
if ($lastuserid != $user->id) { if ($lastuserid != $user->id) {
$lastuserid = $user->id;
// Did the last user end up with a positive permission?
if ($lastuserid !=0) {
if ($hascap[$capability]->perm > 0
|| ($doanything && isset($hascap['moodle/site:candoanything'])
&& $hascap['moodle/site:candoanything']->perm > 0)) {
$c++; $c++;
$newuser = true; } else {
// remove the user from the result set,
// only if we are 'in the page'
if ($limitfrom === 0 || $c >= $limitfrom) {
unset($results[$lastuserid]);
}
}
}
// Did we hit pagination limit?
if ($limitnum !==0 && $c > ($limitfrom+$limitnum)) { // we are done! if ($limitnum !==0 && $c > ($limitfrom+$limitnum)) { // we are done!
break; break;
} }
// New user setup, and state machine init
$newuser = true;
$lastuserid = $user->id;
$hascap = array();
foreach ($caps as $cap) {
// Do not set, unless it's interesting
// (we evaluate for isset() later)
if ($defaultroleinteresting) {
if (isset($roleperms[$cap][$CFG->defaultuserroleid])) {
$defroleperms = $roleperms[$cap][$CFG->defaultuserroleid];
$hascap[$cap] = new StdClass;
$hascap[$cap]->perm = $defroleperms->perm;
$hascap[$cap]->rcdepth = $defroleperms->rcdepth;
$hascap[$cap]->radepth = 1; // site-level
}
}
unset($defroleperms);
}
unset($cap);
// if we are 'in the page', also add the rec
// to the results...
if ($limitfrom === 0 || $c >= $limitfrom) {
$results[$user->id] = $user; // trivial
}
} else { } else {
$newuser = false; $newuser = false;
} }
if ($limitfrom !==0 && $c <= $limitfrom) {
continue;
}
// provide a default enrolment where needed
if (empty($user->roleid)) {
$user->roleid = (int)$CFG->defaultuserroleid;
$user->depth = 1;
}
// Add the users - adding the perms that win
if ($newuser) {
$users[$user->id] = $user; // trivial
} else {
$inplace = $users[$user->id];
// //
// Resolve who wins, in order of precendence // Compute which permission/roleassignment/rolecap
// wins for each capability we are walking
//
if (!$newuser || $defaultroleinteresting) {
foreach ($caps as $cap) {
if (!isset($roleperms[$cap][$user->roleid])) {
// nothing set for this cap - skip
continue;
}
// We explicitly clone here as we
// add more properties to it
// that must stay separate from the
// original roleperm data structure
$rp = clone($roleperms[$cap][$user->roleid]);
$rp->radepth = $user->depth;
// Trivial case, we are the first to set
if ($hascap[$cap]->radepth === 0) {
$hascap[$cap] = $rp;
}
//
// Resolve who prevails, in order of precendence
// - Prohibits always wins // - Prohibits always wins
// - Locality of RA // - Locality of RA
// - Locality of RC // - Locality of RC
// //
//// Prohibits... //// Prohibits...
if ($roleperms[$user->roleid]->perm == CAP_PROHIBIT) { if ($rp->perm === CAP_PROHIBIT) {
$users[$user->id] = $user; $hascap[$cap] = $rp;
continue; continue;
} }
if ($roleperms[$inplace->roleid]->perm == CAP_PROHIBIT) { if ($hascap[$cap]->perm === CAP_PROHIBIT) {
continue; continue;
} }
// Locality of RA -- we order by depth DESC // Locality of RA - the look is ordered by depth DESC
// so what's in $users already should win, unless its perm is 0 // so from local to general -
// Higher RA loses to local RA... unless perm===0
// Higher RA loses to local RA... /// Thanks to the order of the records, $rp->radepth <= $hascap[$cap]->radepth
if ($user->depth < $inplace->depth) { /// _except_ for the default enrolment -- when it's interesting
if (!empty($users[$user->id]->mergedperm)) { if ($rp->radepth > $hascap[$cap]->radepth) {
// Wider RA loses to local RA... // override the default enrolment -
// this is BUGGY because the default enrolment
// cannot "resolve" a pair of conflicted lower-level RAs
// TODO: Move the default enrolment to the tail of processing
$hascap[$cap] = $rp;
}
if ($rp->radepth < $hascap[$cap]->radepth) {
if ($hascap[$cap]->perm!==0) {
// Wider RA loses to local RAs...
continue; continue;
} else { } else {
// "Resolve conflict" case, more local RAs had cancelled eachother // "Higher RA resolves conflict" case,
$users[$user->id] = $user; // local RAs had cancelled eachother
$hascap[$cap] = $rp;
continue; continue;
} }
} }
// Same-level RA - Locality of RC wins // Same ralevel - locality of RC wins
if ($roleperms[$user->roleid]->depth > $roleperms[$inplace->roleid]->depth) { if ($rp->rcdepth > $hascap[$cap]->rcdepth) {
$users[$user->id] = $user; $hascap[$cap] = $rp;
continue; continue;
} }
if ($roleperms[$user->roleid]->depth < $roleperms[$inplace->roleid]->depth) { if ($rp->rcdepth > $hascap[$cap]->rcdepth) {
continue; continue;
} }
// We match depth - add them // We match depth - add them
if (isset($inplace->mergedperm)) { $hascap[$cap]->perm += $rp->perm;
$users[ $user->id ]->mergedperm = ($inplace->mergedperm }
+ $roleperms[$user->roleid]->perm); }
} else { // Prune last entry if necessary
$users[ $user->id ]->mergedperm = ($roleperms[$inplace->roleid]->perms if (!($hascap[$capability]->perm > 0
+ $roleperms[$user->roleid]->perm); || ($doanything && isset($hascap['moodle/site:candoanything'])
&& $hascap['moodle/site:candoanything']->perm > 0))) {
// remove the user from the result set,
// only if we are 'in the page'
if (isset($results[$lastuserid])) {
unset($results[$lastuserid]);
} }
} }
} }
return $users; //error_log(print_r($results,1));
return $results;
} }
/** /**