Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions packages/web/lib/plugins/site/class/site.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,119 @@ public function assocSetter($assocItem, $alterItem = '', $implicitCall = false)
}
return $this;
}
/**
* Whether this user's view is bounded by site membership.
*
* @param int $userID The user to test.
*
* @return bool
*/
public static function userIsRestricted($userID)
{
$userID = (int)$userID;
if ($userID < 1) {
return false;
}
$flags = self::getSubObjectIDs(
'SiteUserRestriction',
array('userID' => $userID),
'isRestricted'
);
return (bool)(isset($flags[0]) ? $flags[0] : false);
}
/**
* The sites this user belongs to.
*
* @param int $userID The user to look up.
*
* @return array
*/
public static function userSiteIDs($userID)
{
return (array)self::getSubObjectIDs(
'SiteUserAssociation',
array('userID' => (int)$userID),
'siteID'
);
}
/**
* The hosts belonging to any of these sites.
*
* @param array $siteIDs The sites.
*
* @return array
*/
public static function hostIDsForSites($siteIDs)
{
return (array)self::getSubObjectIDs(
'SiteHostAssociation',
array('siteID' => (array)$siteIDs),
'hostID'
);
}
/**
* The groups holding one or more hosts of these sites.
*
* @param array $siteIDs The sites.
*
* @return array
*/
public static function groupIDsForSites($siteIDs)
{
$hostIDs = self::hostIDsForSites($siteIDs);
if (count($hostIDs) < 1) {
return array();
}
return (array)self::getSubObjectIDs(
'GroupAssociation',
array('hostID' => $hostIDs),
'groupID'
);
}
/**
* The object ids $userID may see for $classname.
*
* THE RETURN IS A TRI-STATE and the distinction is the whole point:
*
* null no boundary applies -- leave the caller's set alone
* array(...) narrow to exactly these ids
* array() a real answer meaning "nothing", NOT "no boundary"
*
* null is the only value that means "unbounded". Treating an empty
* array as unbounded -- which is what any `if (!$ids)` test does -- is
* how a user entitled to nothing ends up seeing everything, so callers
* must test `null ===` and nothing looser.
*
* This is the single statement of the membership rule. The management
* pages reach it through AddSiteFilterSearch and the API reaches it
* through AddSiteAPI; if the two ever disagree about who may see what,
* the boundary is decorative.
*
* @param string $classname The class being listed or fetched.
* @param int $userID The acting user.
*
* @return array|null
*/
public static function scopedObjectIDs($classname, $userID)
{
$classname = strtolower((string)$classname);
// Only what the plugin actually associates. Everything else --
// images, snapins, storage nodes, the association tables -- has no
// site boundary to apply, and returning an id list for one would
// narrow lookups the plugin knows nothing about.
if (!in_array($classname, array('host', 'group'), true)) {
return null;
}
$userID = (int)$userID;
if (!self::userIsRestricted($userID)) {
return null;
}
$siteIDs = self::userSiteIDs($userID);
if (count($siteIDs) < 1) {
return array();
}
return 'group' === $classname
? self::groupIDsForSites($siteIDs)
: self::hostIDsForSites($siteIDs);
}
}
54 changes: 54 additions & 0 deletions packages/web/lib/plugins/site/hooks/addsiteapi.hook.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ public function __construct()
$this,
'adjustMassInfo'
)
)
->register(
'API_SCOPE_IDS',
array(
$this,
'scopeIDs'
)
);
}
/**
Expand Down Expand Up @@ -189,6 +196,53 @@ public function adjustMassInfo($arguments)
break;
}
}
/**
* Narrows an API read to the acting user's sites.
*
* Until this existed the plugin's boundary was a management-page
* feature: the only filtering hook is AddSiteFilterSearch, registered
* on HOST_DATA and GROUP_DATA, and both handlers switch on the global
* $node/$sub the pages set. Nothing under api/ fires those events, so
* a site-restricted user saw their site in the grid and every host on
* the server through /fog/host/list -- on the same credentials, and
* without an API token, because Route skips API auth entirely when a
* management session is already valid.
*
* Sets $arguments['ids'] only when a boundary actually applies. Left
* alone it stays null, which is the caller's "no narrowing" value; an
* EMPTY array set here is a real answer meaning the user may see
* nothing. See Site::scopedObjectIDs() for why those must not be
* collapsed.
*
* @param mixed $arguments The arguments to modify.
*
* @return void
*/
public function scopeIDs($arguments)
{
if (!in_array($this->node, (array)self::$pluginsinstalled)) {
return;
}
// No acting user means no boundary to apply -- the service daemons
// and the status endpoints reach Route::ids()/names() with nobody
// logged in, and narrowing those to a site would break imaging
// rather than protect anything.
if (!self::$FOGUser || !self::$FOGUser->isValid()) {
return;
}
$scope = Site::scopedObjectIDs(
$arguments['classname'],
self::$FOGUser->get('id')
);
if (null === $scope) {
return;
}
$arguments['ids'] = array_values(
array_unique(
array_map('intval', (array)$scope)
)
);
}
/**
* This function changes the getter to enact on this particular item.
*
Expand Down
42 changes: 14 additions & 28 deletions packages/web/lib/plugins/site/hooks/addsitefiltersearch.hook.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,17 @@ public function hostData($arguments)
case 'host':
switch ($sub) {
case 'search':
// Narrowed against $siteHosts rather than by a
// second SiteHostAssociation lookup of its own:
// the membership rule lives in Site now, and two
// statements of who may see what is a boundary
// that is decorative the first time they differ.
$hostsID = self::getClass('HostManager')->search('');
$hosts = self::getSubObjectIDs(
'SiteHostAssociation',
array('hostID' => $hostsID,'siteID'=>$siteIDbyUser),
'hostID'
$hosts = array_values(
array_intersect(
array_map('intval', (array)$hostsID),
array_map('intval', (array)$siteHosts)
)
);
break;
case 'list':
Expand Down Expand Up @@ -212,12 +218,7 @@ public function groupData($arguments)
*/
public function isRestricted($userid)
{
$userRestrictions = self::getSubObjectIDs(
'SiteUserRestriction',
array('userID' => $userid),
'isRestricted'
);
return $userRestrictions[0];
return Site::userIsRestricted($userid);
}
/**
* Get site IDs where the user is associated.
Expand All @@ -228,12 +229,7 @@ public function isRestricted($userid)
*/
public function getSiteIDbyUser($userID)
{
$find = array('userID' => $userID);
return self::getSubObjectIDs(
'SiteUserAssociation',
$find,
'siteID'
);
return Site::userSiteIDs($userID);
}

/**
Expand All @@ -245,12 +241,7 @@ public function getSiteIDbyUser($userID)
*/
public function getHostIDbySite($siteIDs)
{
$find = array('siteID' => $siteIDs);
return self::getSubObjectIDs(
'SiteHostAssociation',
$find,
'hostID'
);
return Site::hostIDsForSites($siteIDs);
}
/**
* Get the group IDs which have one or more hosts of the user locations.
Expand All @@ -261,11 +252,6 @@ public function getHostIDbySite($siteIDs)
*/
public function getGroupIDbySite($siteIDbyUser)
{
$siteHosts = $this->getHostIDbySite($siteIDbyUser);
return self::getSubObjectIDs(
'GroupAssociation',
array('hostID' => $siteHosts),
'groupID'
);
return Site::groupIDsForSites($siteIDbyUser);
}
}
Loading