silverstripe-framework/security/Group.php
Ingo Schommer bd2b9efede API CHANGE Member->canView() checks for ADMIN or CMS_ACCESS_SecurityAdmin access if not viewing the currently logged-in member. If permissions are enforced in custom interfaces (e.g. social networking frontends), this will impact the output. To loosen permissions, override or decorate Member->canView()
ENHANCEMENT Added Group->canDelete() AND Member->canView()
ENHANCEMENT Making Member->can*() and Group->can*() methods decoratable

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@71327 467b73ca-7a2a-4603-9d3b-597d59a354a9
2009-02-03 23:33:28 +00:00

381 lines
11 KiB
PHP

<?php
/**
* A security group.
*
* @package sapphire
* @subpackage security
*/
class Group extends DataObject {
static $db = array(
"Title" => "Varchar",
"Description" => "Text",
"Code" => "Varchar",
"Locked" => "Boolean",
"Sort" => "Int",
"IPRestrictions" => "Text",
);
static $has_one = array(
"Parent" => "Group",
);
static $has_many = array(
"Permissions" => "Permission",
"Groups" => "Group"
);
static $many_many = array(
"Members" => "Member",
);
static $extensions = array(
"Hierarchy",
);
/**
* Caution: Only call on instances, not through a singleton.
*
* @return FieldSet
*/
public function getCMSFields() {
$fields = new FieldSet(
new TabSet("Root",
new Tab(_t('SecurityAdmin.MEMBERS', 'Members'),
new TextField("Title", $this->fieldLabel('Title')),
$memberList = new MemberTableField(
$this,
"Members",
$this,
null,
false
)
),
new Tab(_t('SecurityAdmin.PERMISSIONS', 'Permissions'),
new LiteralField(
"",
"<p>" .
_t('SecurityAdmin.ADVANCEDONLY',
"This section is for advanced users only.
See <a href=\"http://doc.silverstripe.com/doku.php?id=permissions:codes\" target=\"_blank\">this page</a>
for more information."
) .
"</p>"
),
new TableField(
"Permissions",
"Permission",
array(
"Code" => _t('SecurityAdmin.CODE', 'Code'),
"Arg" => _t('SecurityAdmin.OPTIONALID', 'Optional ID'),
),
array(
"Code" => "PermissionDropdownField",
"Arg" => "TextField",
),
"GroupID",
$this->ID
)
),
new Tab(_t('Security.IPADDRESSES', 'IP Addresses'),
new LiteralField("", _t('SecurityAdmin.IPADDRESSESHELP',"<p>You can restrict this group to a particular
IP address range (one range per line). <br />Ranges can be in any of the following forms: <br />
203.96.152.12<br />
203.96.152/24<br />
203.96/16<br />
203/8<br /><br />If you enter one or more IP address ranges in this box, then members will only get
the rights of being in this group if they log on from one of the valid IP addresses. It won't prevent
people from logging in. This is because the same user might have to log in to access parts of the
system without IP address restrictions.")),
new TextareaField("IPRestrictions", "IP Ranges", 10)
)
)
);
if(!Permission::check('EDIT_PERMISSIONS')) {
$fields->removeFieldFromTab('Root', 'Permissions');
$fields->removeFieldFromTab('Root', 'IP Addresses');
}
$memberList->setController($this);
$memberList->setPermissions(array('show', 'edit', 'delete', 'export', 'add'));
$memberList->setParentClass('Group');
$memberList->setPopupCaption(_t('SecurityAdmin.VIEWUSER', 'View User'));
$fields->push($idField = new HiddenField("ID"));
$this->extend('updateCMSFields', $fields);
return $fields;
}
function fieldLabels() {
$labels = parent::fieldLabels();
$labels['Title'] = _t('SecurityAdmin.GROUPNAME', 'Group name');
$labels['Description'] = _t('Group.Description', 'Description');
$labels['Code'] = _t('Group.Code', 'Group Code', PR_MEDIUM, 'Programmatical code identifying a group');
$labels['Locked'] = _t('Group.Locked', 'Locked?', PR_MEDIUM, 'Group is locked in the security administration area');
$labels['Sort'] = _t('Group.Sort', 'Sort Order');
$labels['IPRestrictions'] = _t('Group.IPRestrictions', 'IP Address Restrictions');
$labels['Parent'] = _t('Group.Parent', 'Parent Group', PR_MEDIUM, 'One group has one parent group');
$labels['Permissions'] = _t('Group.has_many_Permissions', 'Permissions', PR_MEDIUM, 'One group has many permissions');
$labels['Members'] = _t('Group.many_many_Members', 'Members', PR_MEDIUM, 'One group has many members');
return $labels;
}
/**
* Add a member to a group.
*
* @param DataObject $member
* @param string $groupcode
*/
static function addToGroupByName($member, $groupcode) {
$group = DataObject::get_one('Group', "\"Code\" = '" . Convert::raw2sql($groupcode). "'");
if($group) {
$member->Groups()->add($group);
$member->write();
}
}
/**
* Overloaded getter.
*
* @TODO Where is this used, why is this overloaded?
*
* @param $limit string SQL
* @param $offset int
* @param $filter string SQL
* @param $sort string SQL
* @param $join string SQL
* @return ComponentSet
*/
public function Members($limit = "", $offset = "", $filter = "", $sort = "", $join = "") {
$table = "Group_Members";
if($filter) $filter = is_array($filter) ? $filter : array($filter);
if( is_numeric( $limit ) ) {
if( is_numeric( $offset ) )
$limit = "$limit OFFSET $offset";
else
$limit = "$limit OFFSET 0";
} else {
$limit = "";
}
// Get all of groups that this group contains
$groupFamily = implode(", ", $this->collateFamilyIDs());
$filter[] = "\"$table\".\"GroupID\" IN ($groupFamily)";
$join .= " INNER JOIN \"$table\" ON \"$table\".\"MemberID\" = \"Member\".\"ID\"" . Convert::raw2sql($join);
$result = singleton("Member")->instance_get(
$filter,
$sort,
$join,
$limit,
"ComponentSet" // datatype
);
if(!$result) $result = new ComponentSet();
$result->setComponentInfo("many-to-many", $this, "Group", $table, "Member");
foreach($result as $item) $item->GroupID = $this->ID;
return $result;
}
public function map($filter = "", $sort = "", $blank="") {
$ret = new SQLMap(singleton('Group')->extendedSQL($filter, $sort));
if($blank){
$blankGroup = new Group();
$blankGroup->Title = $blank;
$blankGroup->ID = 0;
$ret->getItems()->shift($blankGroup);
}
return $ret;
}
/**
* Return a set of this record's "family" of IDs - the IDs of
* this record and all its descendants.
* @return array
*/
public function collateFamilyIDs() {
$familyIDs = array();
$chunkToAdd = array(array("ID" => $this->ID));
while($chunkToAdd) {
$idList = null;
foreach($chunkToAdd as $item) {
$idList[] = $item['ID'];
$familyIDs[] = $item['ID'];
}
$idList = implode(',',$idList);
// Get the children of *all* the groups identified in the previous chunk.
// This minimises the number of SQL queries necessary
$sql = $this->extendedSQL("\"ParentID\" IN ($idList)", "");
$chunkToAdd = $sql->execute();
if(!$chunkToAdd->numRecords()) $chunkToAdd = null;
}
return $familyIDs;
}
/**
* Returns an array of the IDs of this group and all its parents
*/
public function collateAncestorIDs() {
$parent = $this;
while(isset($parent)) {
$items[] = $parent->ID;
$parent = $parent->Parent;
}
return $items;
}
/**
* This isn't a decendant of SiteTree, but needs this in case
* the group is "reorganised";
*/
function cmsCleanup_parentChanged() {
}
/**
* Override this so groups are ordered in the CMS
*/
public function stageChildren() {
return DataObject::get('Group', "\"Group\".\"ParentID\" = " . (int)$this->ID . " AND \"Group\".\"ID\" != " . (int)$this->ID, '"Sort"');
}
public function TreeTitle() {
if($this->hasMethod('alternateTreeTitle')) return $this->alternateTreeTitle();
else return $this->Title;
}
/**
* Overloaded to ensure the code is always descent.
*/
public function setCode($val){
$this->setField("Code",SiteTree::generateURLSegment($val));
}
function onBeforeWrite() {
parent::onBeforeWrite();
if(stripos($this->Code, 'new-') === 0) {
$this->setCode($this->Title);
}
}
/**
* Checks for permission-code CMS_ACCESS_SecurityAdmin.
* If the group has ADMIN permissions, it requires the user to have ADMIN permissions as well.
*
* @param $member Member
* @return boolean
*/
public function canEdit($member = null) {
if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
// DEPRECATED 2.3: use canView() instead
$results = $this->extend('alternateCanView', $member);
if($results && is_array($results)) if(!min($results)) return false;
// decorated access checks
$results = $this->extend('canEdit', $member);
if($results && is_array($results)) if(!min($results)) return false;
if(
// either we have an ADMIN
(bool)Permission::checkMember($member, "ADMIN")
|| (
// or a privileged CMS user and a group without ADMIN permissions.
// without this check, a user would be able to add himself to an administrators group
// with just access to the "Security" admin interface
Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin") &&
!DataObject::get("Permission", "GroupID = $this->ID AND Code = 'ADMIN'")
)
) {
return true;
}
return false;
}
/**
* Checks for permission-code CMS_ACCESS_SecurityAdmin.
*
* @param $member Member
* @return boolean
*/
public function canView($member = null) {
if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
// DEPRECATED 2.3: use canView() instead
$results = $this->extend('alternateCanView', $member);
if($results && is_array($results)) if(!min($results)) return false;
// decorated access checks
$results = $this->extend('canView', $member);
if($results && is_array($results)) if(!min($results)) return false;
// user needs access to CMS_ACCESS_SecurityAdmin
if(Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin")) return true;
return false;
}
public function canDelete($member = null) {
if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
// decorated access checks
$results = $this->extend('canDelete', $member);
if($results && is_array($results)) if(!min($results)) return false;
return $this->canEdit($member);
}
/**
* Returns all of the children for the CMS Tree.
* Filters to only those groups that the current user can edit
*/
function AllChildrenIncludingDeleted() {
$children = $this->extInstance('Hierarchy')->AllChildrenIncludingDeleted();
$filteredChildren = new DataObjectSet();
if($children) foreach($children as $child) {
if($child->canView()) $filteredChildren->push($child);
}
return $filteredChildren;
}
/**
* Returns true if the given IP address is granted access to this group.
* For unrestricted groups, this always returns true.
*/
function allowedIPAddress($ip) {
if(!$this->IPRestrictions) return true;
if(!$ip) return false;
$ipPatterns = explode("\n", $this->IPRestrictions);
foreach($ipPatterns as $ipPattern) {
$ipPattern = trim($ipPattern);
if(preg_match('/^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$/', $ipPattern, $matches)) {
if($ip == $ipPattern) return true;
} else if(preg_match('/^([0-9]+\.[0-9]+\.[0-9]+)\/24$/', $ipPattern, $matches)
|| preg_match('/^([0-9]+\.[0-9]+)\/16$/', $ipPattern, $matches)
|| preg_match('/^([0-9]+)\/8$/', $ipPattern, $matches)) {
if(substr($ip, 0, strlen($matches[1])) == $matches[1]) return true;
}
}
return false;
}
}
?>