Version: current

      1 <?php
      2 /**
      3  * Represents a permission assigned to a group.
      4  * @package sapphire
      5  * @subpackage security
      6  */
      7 class Permission extends DataObject {
      8
      9   // the (1) after Type specifies the DB default value which is needed for
     10     // upgrades from older SilverStripe versions
     11     static $db = array(
     12         "Code" => "Varchar",
     13         "Arg" => "Int",
     14         "Type" => "Int(1)"
     15     );
     16     static $has_one = array(
     17         "Group" => "Group"
     18     );
     19     static $indexes = array(
     20         "Code" => true
     21     );
     22     static $defaults = array(
     23         "Type" => 1
     24     );
     25     static $has_many = array();
     26
     27     static $many_many = array();
     28
     29     static $belongs_many_many = array();
     30
     31     /**
     32      * This is the value to use for the "Type" field if a permission should be
     33      * granted.
     34      */
     35     const GRANT_PERMISSION = 1;
     36
     37     /**
     38      * This is the value to use for the "Type" field if a permission should be
     39      * denied.
     40      */
     41     const DENY_PERMISSION = -1;
     42
     43     /**
     44      * This is the value to use for the "Type" field if a permission should be
     45      * inherited.
     46      */
     47     const INHERIT_PERMISSION = 0;
     48
     49
     50     /**
     51      * Method to globally disable "strict" checking, which means a permission
     52      * will be granted if the key does not exist at all.
     53      *
     54      * @var bool
     55      */
     56     static $declared_permissions = null;
     57
     58   /**
     59      * Linear list of declared permissions in the system.
     60      *
     61      * @var array
     62      */
     63     protected static $declared_permissions_list = null;
     64
     65     /**
     66      * @var $strict_checking Boolean Method to globally disable "strict" checking,
     67      * which means a permission will be granted if the key does not exist at all.
     68      */
     69     static $strict_checking = true;
     70
     71     /**
     72      * If this setting is set, then permissions can imply other permissions
     73      *
     74      * @var bool
     75      */
     76     static $implied_permissions = false;
     77
     78     /**
     79      * Set to false to prevent the 'ADMIN' permission from implying all
     80      * permissions in the system
     81      *
     82      * @var bool
     83      */
     84     static $admin_implies_all = true;
     85
     86     /**
     87      * a list of permission codes which doesn't appear in the Permission list
     88      * when make the {@link PermissionCheckboxSetField}
     89      * @var array;
     90      */
     91     static $hidden_permissions = array();
     92
     93     /**
     94      * Check that the current member has the given permission.
     95      *
     96      * @param string $code Code of the permission to check (case-sensitive)
     97      * @param string $arg Optional argument (e.g. a permissions for a specific page)
     98      * @param int|Member $member Optional member instance or ID. If set to NULL, the permssion
     99      *  will be checked for the current user
    100      * @param bool $strict Use "strict" checking (which means a permission
    101      *  will be granted if the key does not exist at all)?
    102      * @return int|bool The ID of the permission record if the permission
    103      *  exists; FALSE otherwise. If "strict" checking is
    104      *  disabled, TRUE will be returned if the permission does not exist at all.
    105      */
    106     public static function check($code, $arg = "any", $member = null, $strict = true) {
    107         if(!$member) {
    108             if(!Member::currentUserID()) {
    109                 return false;
    110             }
    111             $member = Member::currentUserID();
    112         }
    113
    114         return self::checkMember($member, $code, $arg, $strict);
    115     }
    116
    117     /**
    118      * Permissions cache.  The format is a map, where the keys are member IDs, and the values are
    119      * arrays of permission codes.
    120      */
    121     private static $cache_permissions = array();
    122
    123     /**
    124      * Flush the permission cache, for example if you have edited group membership or a permission record.
    125      * @todo Call this whenever Group_Members is added to or removed from
    126      */
    127     public static function flush_permission_cache() {
    128         self::$cache_permissions = array();
    129     }
    130
    131     /**
    132      * Check that the given member has the given permission.
    133      *
    134      * @param int|Member memberID The ID of the member to check. Leave blank for the current member.
    135      *  Alternatively you can use a member object.
    136      * @param string|array $code Code of the permission to check (case-sensitive)
    137      * @param string $arg Optional argument (e.g. a permissions for a specific page)
    138      * @param bool $strict Use "strict" checking (which means a permission
    139      *  will be granted if the key does not exist at all)?
    140      * @return int|bool The ID of the permission record if the permission
    141      *  exists; FALSE otherwise. If "strict" checking is
    142      *  disabled, TRUE will be returned if the permission does not exist at all.
    143      */
    144     public static function checkMember($member, $code, $arg = "any", $strict = true) {
    145         if(!$member) {
    146             $memberID = $member = Member::currentUserID();
    147         } else {
    148             $memberID = (is_object($member)) ? $member->ID : $member;
    149         }
    150
    151         if($arg == 'any') {
    152             // Cache the permissions in memory
    153             if(!isset(self::$cache_permissions[$memberID])) {
    154                 self::$cache_permissions[$memberID] = self::permissions_for_member($memberID);
    155             }
    156
    157             // If $admin_implies_all was false then this would be inefficient, but that's an edge
    158             // case and this keeps the code simpler
    159             if(!is_array($code)) $code = array($code);
    160             if(self::$admin_implies_all) $code[] = "ADMIN";
    161
    162             // Multiple $code values - return true if at least one matches, ie, intersection exists
    163             return (bool)array_intersect($code, self::$cache_permissions[$memberID]);
    164         }
    165
    166         // The following code should only be used if you're not using the "any" arg.  This is kind
    167         // of obselete functionality and could possibly be deprecated.
    168
    169         $groupList = self::groupList($memberID);
    170         if(!$groupList) return false;
    171
    172         $groupCSV = implode(", ", $groupList);
    173
    174         // Arg component
    175         switch($arg) {
    176             case "any":
    177                 $argClause = "";
    178                 break;
    179             case "all":
    180                 $argClause = " AND \"Arg\" = -1";
    181                 break;
    182             default:
    183                 if(is_numeric($arg)) {
    184                     $argClause = "AND \"Arg\" IN (-1, $arg) ";
    185                 } else {
    186                     user_error("Permission::checkMember: bad arg '$arg'", E_USER_ERROR);
    187                 }
    188         }
    189
    190         if(is_array($code)) {
    191             $SQL_codeList = "'" . implode("', '", Convert::raw2sql($code)) . "'";
    192         } else {
    193             $SQL_codeList = "'" . Convert::raw2sql($code) . "'";
    194         }
    195
    196         $SQL_code = Convert::raw2sql($code);
    197
    198         $adminFilter = (self::$admin_implies_all) ?  ",'ADMIN'" : '';
    199
    200         // Raw SQL for efficiency
    201         $permission = DB::query("
    202             SELECT \"ID\"
    203             FROM \"Permission\"
    204             WHERE (
    205                 \"Code\" IN ($SQL_codeList $adminFilter)
    206                 AND \"Type\" = " . self::GRANT_PERMISSION . "
    207                 AND \"GroupID\" IN ($groupCSV)
    208                 $argClause
    209             )
    210         ")->value();
    211
    212         if($permission) return $permission;
    213
    214         // Strict checking disabled?
    215         if(!self::$strict_checking || !$strict) {
    216             $hasPermission = DB::query("
    217                 SELECT COUNT(*)
    218                 FROM \"Permission\"
    219                 WHERE (
    220                     (\"Code\" IN '$SQL_code')'
    221                     AND (\"Type\" = " . self::GRANT_PERMISSION . ")
    222                 )
    223             ")->value();
    224
    225             if(!$hasPermission) return;
    226         }
    227
    228         return false;
    229     }
    230
    231     /**
    232      * Get all the 'any' permission codes available to the given member.
    233      * @return array();
    234      */
    235     public static function permissions_for_member($memberID) {
    236         $groupList = self::groupList($memberID);
    237         if($groupList) {
    238             $groupCSV = implode(", ", $groupList);
    239
    240             // Raw SQL for efficiency
    241             return array_unique(DB::query("
    242                 SELECT \"Code\"
    243                 FROM \"Permission\"
    244                 WHERE \"Type\" = " . self::GRANT_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
    245
    246                 UNION
    247
    248                 SELECT \"Code\"
    249                 FROM \"PermissionRoleCode\" AS PRC
    250                 INNER JOIN \"PermissionRole\" AS PR ON PRC.\"RoleID\" = PR.\"ID\"
    251                 INNER JOIN \"Group_Roles\" AS GR ON GR.\"PermissionRoleID\" = PR.\"ID\"
    252                 WHERE \"GroupID\" IN ($groupCSV)
    253             ")->column());
    254
    255         } else {
    256             return array();
    257         }
    258     }
    259
    260
    261     /**
    262      * Get the list of groups that the given member belongs to.
    263      *
    264      * Call without an argument to get the groups that the current member
    265      * belongs to. In this case, the results will be session-cached.
    266      *
    267      * @param int $memberID The ID of the member. Leave blank for the current
    268      *                      member.
    269      * @return array Returns a list of group IDs to which the member belongs
    270      *               to or NULL.
    271      */
    272     public static function groupList($memberID = null) {
    273         // Default to current member, with session-caching
    274         if(!$memberID) {
    275             $member = Member::currentUser();
    276             if($member && isset($_SESSION['Permission_groupList'][$member->ID]))
    277                 return $_SESSION['Permission_groupList'][$member->ID];
    278         } else {
    279             $member = DataObject::get_by_id("Member", $memberID);
    280         }
    281
    282         if($member) {
    283             // Build a list of the IDs of the groups.  Most of the heavy lifting
    284             // is done by Member::Groups
    285             // NOTE: This isn't effecient; but it's called once per session so
    286             // it's a low priority to fix.
    287             $groups = $member->Groups();
    288             $groupList = array();
    289
    290             if($groups) {
    291                 foreach($groups as $group)
    292                     $groupList[] = $group->ID;
    293             }
    294
    295
    296             // Session caching
    297             if(!$memberID) {
    298                 $_SESSION['Permission_groupList'][$member->ID] = $groupList;
    299             }
    300
    301             return isset($groupList) ? $groupList : null;
    302         }
    303     }
    304
    305
    306     /**
    307      * Grant the given permission code/arg to the given group
    308      *
    309      * @param int $groupID The ID of the group
    310      * @param string $code The permission code
    311      * @param string Optional: The permission argument (e.g. a page ID).
    312      * @returns Permission Returns the new permission object.
    313      */
    314     public static function grant($groupID, $code, $arg = "any") {
    315         $perm = new Permission();
    316         $perm->GroupID = $groupID;
    317         $perm->Code = $code;
    318         $perm->Type = self::GRANT_PERMISSION;
    319
    320         // Arg component
    321         switch($arg) {
    322             case "any":
    323                 break;
    324             case "all":
    325                 $perm->Arg = -1;
    326             default:
    327                 if(is_numeric($arg)) {
    328                     $perm->Arg = $arg;
    329                 } else {
    330                     user_error("Permission::checkMember: bad arg '$arg'",
    331                                         E_USER_ERROR);
    332                 }
    333         }
    334
    335         $perm->write();
    336         return $perm;
    337     }
    338
    339
    340     /**
    341      * Deny the given permission code/arg to the given group
    342      *
    343      * @param int $groupID The ID of the group
    344      * @param string $code The permission code
    345      * @param string Optional: The permission argument (e.g. a page ID).
    346      * @returns Permission Returns the new permission object.
    347      */
    348     public static function deny($groupID, $code, $arg = "any") {
    349         $perm = new Permission();
    350         $perm->GroupID = $groupID;
    351         $perm->Code = $code;
    352         $perm->Type = self::DENY_PERMISSION;
    353
    354         // Arg component
    355         switch($arg) {
    356             case "any":
    357                 break;
    358             case "all":
    359                 $perm->Arg = -1;
    360             default:
    361                 if(is_numeric($arg)) {
    362                     $perm->Arg = $arg;
    363                 } else {
    364                     user_error("Permission::checkMember: bad arg '$arg'",
    365                                         E_USER_ERROR);
    366                 }
    367         }
    368
    369         $perm->write();
    370         return $perm;
    371     }
    372
    373     /**
    374      * Returns all members for a specific permission.
    375      *
    376      * @param $code String|array Either a single permission code, or a list of permission codes
    377      * @return DataObjectSet Returns a set of member that have the specified
    378      *                       permission.
    379      */
    380     public static function get_members_by_permission($code) {
    381         $toplevelGroups = self::get_groups_by_permission($code);
    382         if (!$toplevelGroups) return false;
    383
    384         $groupIDs = array();
    385         foreach($toplevelGroups as $group) {
    386             $familyIDs = $group->collateFamilyIDs();
    387             if(is_array($familyIDs)) {
    388                 $groupIDs = array_merge($groupIDs, array_values($familyIDs));
    389             }
    390         }
    391
    392         if(!count($groupIDs))
    393             return false;
    394
    395         $members = DataObject::get(
    396             Object::getCustomClass('Member'),
    397             $_filter = "\"Group\".\"ID\" IN (" . implode(",",$groupIDs) . ")",
    398             $_sort = "",
    399             $_join = "LEFT JOIN \"Group_Members\" ON \"Member\".\"ID\" = \"Group_Members\".\"MemberID\" " .
    400                 "LEFT JOIN \"Group\" ON \"Group_Members\".\"GroupID\" = \"Group\".\"ID\" "
    401         );
    402
    403         return $members;
    404     }
    405
    406     /**
    407      * Return all of the groups that have one of the given permission codes
    408      * @param $codes array|string Either a single permission code, or an array of permission codes
    409      * @return DataObjectSet The matching group objects
    410      */
    411     static function get_groups_by_permission($codes) {
    412         if(!is_array($codes)) $codes = array($codes);
    413
    414         $SQLa_codes = Convert::raw2sql($codes);
    415         $SQL_codes = join("','", $SQLa_codes);
    416
    417         // Via Roles are groups that have the permission via a role
    418         return DataObject::get('Group',
    419             "\"PermissionRoleCode\".\"Code\" IN ('$SQL_codes') OR \"Permission\".\"Code\" IN ('$SQL_codes')",
    420             "",
    421             "LEFT JOIN \"Permission\" ON \"Permission\".\"GroupID\" = \"Group\".\"ID\"
    422             LEFT JOIN \"Group_Roles\" ON \"Group_Roles\".\"GroupID\" = \"Group\".\"ID\"
    423             LEFT JOIN \"PermissionRole\" ON \"Group_Roles\".\"PermissionRoleID\" = \"PermissionRole\".\"ID\"
    424             LEFT JOIN \"PermissionRoleCode\" ON \"PermissionRoleCode\".\"RoleID\" = \"PermissionRole\".\"ID\"");
    425     }
    426
    427
    428     /**
    429      * Get a list of all available permission codes, both defined through the
    430      * {@link PermissionProvider} interface, and all not explicitly defined codes existing
    431      * as a {@link Permission} database record. By default, the results are
    432      * grouped as denoted by {@link Permission_Group}.
    433      *
    434      * @param bool $grouped Group results into an array of permission groups.
    435      * @return array Returns an array of all available permission codes. The
    436      *  array indicies are the permission codes as used in
    437      *  {@link Permission::check()}. The value is a description
    438      *  suitable for using in an interface.
    439      */
    440     public static function get_codes($grouped = true) {
    441         $classes = ClassInfo::implementorsOf('PermissionProvider');
    442
    443         $allCodes = array();
    444         $adminCategory = _t('Permission.AdminGroup', 'Administrator');
    445         $allCodes[$adminCategory]['ADMIN'] = array(
    446             'name' => _t('Permission.FULLADMINRIGHTS', 'Full administrative rights'),
    447             'help' => _t(
    448                 'Permission.FULLADMINRIGHTS_HELP',
    449                 'Implies and overrules all other assigned permissions.'
    450             ),
    451             'sort' => 100000
    452         );
    453
    454         if($classes) foreach($classes as $class) {
    455             $SNG = singleton($class);
    456             if($SNG instanceof TestOnly) continue;
    457
    458             $someCodes = $SNG->providePermissions();
    459             if($someCodes) {
    460                 foreach($someCodes as $k => $v) {
    461                     if (is_array($v)) {
    462                         // There must be a category and name key.
    463                         if (!isset($v['category'])) user_error("The permission $k must have a category key", E_USER_WARNING);
    464                         if (!isset($v['name'])) user_error("The permission $k must have a name key", E_USER_WARNING);
    465
    466                         if (!isset($allCodes[$v['category']])) $allCodes[$v['category']] = array();
    467
    468                         $allCodes[$v['category']][$k] = array(
    469                             'name' => $v['name'],
    470                             'help' => isset($v['help']) ? $v['help'] : null,
    471                             'sort' => isset($v['sort']) ? $v['sort'] : 0
    472                         );
    473
    474                     } else {
    475                         $allCodes[_t('Permission.OtherGroup', 'Other')][$k] = array(
    476                             'name' => $v,
    477                             'help' => null,
    478                             'sort' => 0
    479                         );
    480                     }
    481                 }
    482             }
    483         }
    484
    485         $flatCodeArray = array();
    486         foreach($allCodes as $category) foreach($category as $code => $permission) $flatCodeArray[] = $code;
    487         $otherPerms = DB::query("SELECT DISTINCT \"Code\" From \"Permission\" WHERE \"Code\" != ''")->column();
    488
    489         if($otherPerms) foreach($otherPerms as $otherPerm) {
    490             if(!in_array($otherPerm, $flatCodeArray))
    491                 $allCodes[_t('Permission.OtherGroup', 'Other')][$otherPerm] = array(
    492                     'name' => _t('PermissionCode.' . $otherPerm, $otherPerm),
    493                     'help' => null,
    494                     'sort' => 0
    495                 );
    496         }
    497
    498         // Don't let people hijack ADMIN rights
    499         if(!Permission::check("ADMIN")) unset($allCodes['ADMIN']);
    500
    501         ksort($allCodes);
    502
    503         $returnCodes = array();
    504         foreach($allCodes as $category => $permissions) {
    505             if($grouped) {
    506                 uasort($permissions, array(__CLASS__, 'sort_permissions'));
    507                 $returnCodes[$category] = $permissions;
    508             } else {
    509                 $returnCodes = array_merge($returnCodes, $permissions);
    510             }
    511         }
    512
    513         return $returnCodes;
    514     }
    515
    516     /**
    517      * Sort permissions based on their sort value, or name
    518      *
    519      */
    520     static function sort_permissions($a, $b) {
    521         if ($a['sort'] == $b['sort']) {
    522             // Same sort value, do alpha instead
    523             return strcmp($a['name'], $b['name']);
    524         } else {
    525             // Just numeric.
    526             return $a['sort'] < $b['sort'] ? -1 : 1;
    527         }
    528     }
    529
    530     /**
    531      * add a permission represented by the $code to the {@link slef::$hidden_permissions} list
    532      *
    533      * @param $code string - the permissions code
    534      * @return void
    535      */
    536     static function add_to_hidden_permissions($code){
    537         self::$hidden_permissions[] = $code;
    538     }
    539
    540     /**
    541      * remove a permission represented by the $code from the {@link slef::$hidden_permissions} list
    542      *
    543      * @param $code string - the permissions code
    544      * @return void
    545      */
    546     static function remove_from_hidden_permissions($code){
    547         self::$hidden_permissions = array_diff(self::$hidden_permissions, array($code));
    548     }
    549
    550     /**
    551      * Declare an array of permissions for the system.
    552      *
    553      * Permissions can be grouped by nesting arrays. Scalar values are always
    554      * treated as permissions.
    555      *
    556      * @param array $permArray A (possibly nested) array of permissions to
    557      *                         declare for the system.
    558      */
    559     static function declare_permissions($permArray) {
    560         if(is_array(self::$declared_permissions)) {
    561             self::$declared_permissions =
    562                 array_merge_recursive(self::$declared_permissions, $permArray);
    563         }
    564         else {
    565             self::$declared_permissions = $permArray;
    566         }
    567     }
    568
    569
    570     /**
    571      * Get a linear list of the permissions in the system.
    572      *
    573      * @return array Linear list of declared permissions in the system.
    574      */
    575     public static function get_declared_permissions_list() {
    576         if(!self::$declared_permissions)
    577             return null;
    578
    579         if(self::$declared_permissions_list)
    580             return self::$declared_permissions_list;
    581
    582         self::$declared_permissions_list = array();
    583
    584         self::traverse_declared_permissions(self::$declared_permissions,
    585                                                                                 self::$declared_permissions_list);
    586
    587         return self::$declared_permissions_list;
    588     }
    589
    590     /**
    591      * Look up the human-readable title for the permission as defined by <code>Permission::declare_permissions</code>
    592      *
    593      * @param $perm Permission code
    594      * @return Label for the given permission, or the permission itself if the label doesn't exist
    595      */
    596     public static function get_label_for_permission($perm) {
    597         $list = self::get_declared_permissions_list();
    598         if(array_key_exists($perm, $list)) return $list[$perm];
    599         return $perm;
    600     }
    601
    602     /**
    603      * Recursively traverse the nested list of declared permissions and create
    604      * a linear list.
    605      *
    606      * @param aeeay $declared Nested structure of permissions.
    607      * @param $list List of permissions in the structure. The result will be
    608      *              written to this array.
    609      */
    610     protected static function traverse_declared_permissions($declared,
    611                                                                                                                     &$list) {
    612         if(!is_array($declared))
    613             return;
    614
    615         foreach($declared as $perm => $value) {
    616             if($value instanceof Permission_Group) {
    617                 $list[] = $value->getName();
    618                 self::traverse_declared_permissions($value->getPermissions(), $list);
    619             } else {
    620                 $list[$perm] = $value;
    621             }
    622         }
    623     }
    624
    625     public function onBeforeWrite() {
    626         parent::onBeforeWrite();
    627
    628         // Just in case we've altered someone's permissions
    629         Permission::flush_permission_cache();
    630     }
    631 }
    632
    633
    634 /**
    635  * Permission_Group class
    636  *
    637  * This class is used to group permissions together for showing on an
    638  * interface.
    639  * @package sapphire
    640  * @subpackage security
    641  */
    642 class Permission_Group {
    643
    644     /**
    645      * Name of the permission group (can be used as label in an interface)
    646      * @var string
    647      */
    648     protected $name;
    649
    650     /**
    651      * Associative array of permissions in this permission group. The array
    652      * indicies are the permission codes as used in
    653      * {@link Permission::check()}. The value is suitable for using in an
    654      * interface.
    655      * @var string
    656      */
    657     protected $permissions = array();
    658
    659
    660     /**
    661      * Constructor
    662      *
    663      * @param string $name Text that could be used as label used in an
    664      *                     interface
    665      * @param array $permissions Associative array of permissions in this
    666      *                           permission group. The array indicies are the
    667      *                           permission codes as used in
    668      *                           {@link Permission::check()}. The value is
    669      *                           suitable for using in an interface.
    670      */
    671     public function __construct($name, $permissions) {
    672         $this->name = $name;
    673         $this->permissions = $permissions;
    674     }
    675
    676     /**
    677      * Get the name of the permission group
    678      *
    679      * @return string Name (label) of the permission group
    680      */
    681     public function getName() {
    682         return $this->name;
    683     }
    684
    685
    686     /**
    687      * Get permissions
    688      *
    689      * @return array Associative array of permissions in this permission
    690      *               group. The array indicies are the permission codes as
    691      *               used in {@link Permission::check()}. The value is
    692      *               suitable for using in an interface.
    693      */
    694     public function getPermissions() {
    695         return $this->permissions;
    696     }
    697 }
    698
    699 ?>
    700

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -