accesslib: get_user_by_capability() - Move capcheck to has_capability_from_rarc()

This fixes the handling of default roles as "tie breakers" for lower
RAs in conflict, and simplifies the code a lot.

The main loop in get_user_by_capability() runs a simpler state machine
that just collects role assignments (roleid and depth), and handles
pagination.

The complex part of the state machine has moved to
has_capability_from_rarc() which will walk the data structures
collected by get_user_by_capability() for each user.

Having all the complex state handling of $hascap there makes things a
lot easier for pagination and general sanity of
get_user_by_capability().

MDL-12452
This commit is contained in:
martinlanghoff 2008-01-06 23:24:25 +00:00
parent d2c5b7a9fa
commit 6c9d86b7a6

View File

@ -4463,6 +4463,20 @@ function get_users_by_capability($context, $capability, $fields='', $sort='',
// state machine to track the cap/perms and at what RA-depth
// and RC-depth they were defined.
//
// So what we do here is:
// - loop over rows, checking pagination limits
// - when we find a new user, if we are in the page add it to the
// $results, and start building $ras array with its role-assignments
// - when we are dealing with the next user, or are at the end of the userlist
// (last rec or last in page), trigger the check-permission idiom
// - the check permission idiom will
// - add the default enrolment if needed
// - call has_capability_from_rarc(), which based on RAs and RCs will return a bool
// (should be fairly tight code ;-) )
// - if the user has permission, all is good, just $c++ (counter)
// - ...else, decrease the counter - so pagination is kept straight,
// and (if we are in the page) remove from the results
//
$results = array();
// pagination controls
@ -4470,27 +4484,17 @@ function get_users_by_capability($context, $capability, $fields='', $sort='',
$limitfrom = (int)$limitfrom;
$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
// Track our last user id so we know when we are dealing
// with a new user...
//
$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);
//
// In this loop, we
// $ras: role assignments, multidimensional array
// treat as a stack - going from local to general
// $ras = (( roleid=> x, $depth=>y) , ( roleid=> x, $depth=>y))
//
while ($user = rs_fetch_next_record($rs)) {
//error_log(" Record: " . print_r($user,1));
@ -4505,9 +4509,12 @@ function get_users_by_capability($context, $capability, $fields='', $sort='',
// 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)) {
if ($defaultroleinteresting) {
// add the role at the end of $ras
$ras[] = array( 'roleid' => $CFG->defaultuserroleid,
'depth' => 1 );
}
if (has_capability_from_rarc($ras, $roleperms, $capability, $doanything)) {
$c++;
} else {
// remove the user from the result set,
@ -4523,26 +4530,13 @@ function get_users_by_capability($context, $capability, $fields='', $sort='',
break;
}
// New user setup, and state machine init
$newuser = true;
// New user setup, and $ras reset
$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);
$ras = array();
if (!empty($user->roleid)) {
$ras[] = array( 'roleid' => (int)$user->roleid,
'depth' => (int)$user->depth );
}
unset($cap);
// if we are 'in the page', also add the rec
// to the results...
@ -4550,97 +4544,139 @@ function get_users_by_capability($context, $capability, $fields='', $sort='',
$results[$user->id] = $user; // trivial
}
} else {
$newuser = false;
// Additional RA for $lastuserid
$ras[] = array( 'roleid'=>(int)$user->roleid,
'depth'=>(int)$user->depth );
}
//
// 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;
} // end while(fetch)
// 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
// - Locality of RA
// - Locality of RC
//
//// Prohibits...
if ($rp->perm === CAP_PROHIBIT) {
$hascap[$cap] = $rp;
continue;
}
if ($hascap[$cap]->perm === CAP_PROHIBIT) {
continue;
}
// Locality of RA - the look is ordered by depth DESC
// so from local to general -
// Higher RA loses to local RA... unless perm===0
/// Thanks to the order of the records, $rp->radepth <= $hascap[$cap]->radepth
/// _except_ for the default enrolment -- when it's interesting
if ($rp->radepth > $hascap[$cap]->radepth) {
// 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;
} else {
// "Higher RA resolves conflict" case,
// local RAs had cancelled eachother
$hascap[$cap] = $rp;
continue;
}
}
// Same ralevel - locality of RC wins
if ($rp->rcdepth > $hascap[$cap]->rcdepth) {
$hascap[$cap] = $rp;
continue;
}
if ($rp->rcdepth > $hascap[$cap]->rcdepth) {
continue;
}
// We match depth - add them
$hascap[$cap]->perm += $rp->perm;
}
// Prune last entry if necessary
if ($lastuserid !=0) {
if ($defaultroleinteresting) {
// add the role at the end of $ras
$ras[] = array( 'roleid' => $CFG->defaultuserroleid,
'depth' => 1 );
}
// Prune last entry if necessary
if (!($hascap[$capability]->perm > 0
|| ($doanything && isset($hascap['moodle/site:candoanything'])
&& $hascap['moodle/site:candoanything']->perm > 0))) {
if (!has_capability_from_rarc($ras, $roleperms, $capability, $doanything)) {
// remove the user from the result set,
// only if we are 'in the page'
if (isset($results[$lastuserid])) {
unset($results[$lastuserid]);
if ($limitfrom === 0 || $c >= $limitfrom) {
if (isset($results[$lastuserid])) {
unset($results[$lastuserid]);
}
}
}
}
//error_log(print_r($results,1));
return $results;
}
/*
* Fast (fast!) utility function to resolve if a capability is granted,
* based on Role Assignments and Role Capabilities.
*
* Used (at least) by get_users_by_capability().
*
* If PHP had fast built-in memoize functions, we could
* add a $contextid parameter and memoize the return values.
*
* @param array $ras - role assignments
* @param array $roleperms - role permissions
* @param string $capability - name of the capability
* @param bool $doanything
* @return boolean
*
*/
function has_capability_from_rarc($ras, $roleperms, $capability, $doanything) {
// Mini-state machine, using $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
$caps = array($capability);
if ($doanything) {
$caps[] = 'moodle/site:candoanything';
}
$hascap = array();
//
// Compute which permission/roleassignment/rolecap
// wins for each capability we are walking
//
foreach ($ras as $ra) {
foreach ($caps as $cap) {
if (!isset($roleperms[$cap][$ra['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][$ra['roleid']]);
$rp->radepth = $ra['depth'];
// Trivial case, we are the first to set
if (!isset($hascap[$cap])) {
$hascap[$cap] = $rp;
}
//
// Resolve who prevails, in order of precendence
// - Prohibits always wins
// - Locality of RA
// - Locality of RC
//
//// Prohibits...
if ($rp->perm === CAP_PROHIBIT) {
$hascap[$cap] = $rp;
continue;
}
if ($hascap[$cap]->perm === CAP_PROHIBIT) {
continue;
}
// Locality of RA - the look is ordered by depth DESC
// so from local to general -
// Higher RA loses to local RA... unless perm===0
/// Thanks to the order of the records, $rp->radepth <= $hascap[$cap]->radepth
if ($rp->radepth > $hascap[$cap]->radepth) {
error_log('Should not happen @ ' . __FUNCTION__.':'.__LINE__);
}
if ($rp->radepth < $hascap[$cap]->radepth) {
if ($hascap[$cap]->perm!==0) {
// Wider RA loses to local RAs...
continue;
} else {
// "Higher RA resolves conflict" case,
// local RAs had cancelled eachother
$hascap[$cap] = $rp;
continue;
}
}
// Same ralevel - locality of RC wins
if ($rp->rcdepth > $hascap[$cap]->rcdepth) {
$hascap[$cap] = $rp;
continue;
}
if ($rp->rcdepth > $hascap[$cap]->rcdepth) {
continue;
}
// We match depth - add them
$hascap[$cap]->perm += $rp->perm;
}
}
if ($hascap[$capability]->perm > 0
|| ($doanything && isset($hascap['moodle/site:candoanything'])
&& $hascap['moodle/site:candoanything']->perm > 0)) {
return true;
}
return false;
}
/**
* gets all the users assigned this role in this context or higher
* @param int roleid (can also be an array of ints!)