*/ ?>

Advanced Permissions w/ Code in concrete5 ver 5.6+

The permissions model changed drastically with the last version of concrete5. I had to migrate a project started last October to the newer core today. This post documents what I found out about updating things after spending all day trying to sort through the core.

The Problem

It seemed like a pretty simple thing, really. Well, maybe not quite that simple...

I have a dashboard page that people can use to create pages that represent communities. There is a 'simple' and a 'advanced' format for each. Those are copied from a hidden node off of the page root to keep page structure, the advanced starts out with mutliple pages beneath it, but the basic does not. The dashboard page is actually creating a custom 'community' object which interfaces with a couple of other custom objects that attach to it, and the page created when the community is added. This allows me to use page attributes on the custom object without creating my own attribute category but still validate them, since the custom object validates the attributes before creating the page / saving the form.  The custom object also has custom lists that allows them to be filtered both on the page attributes and on the associated custom objects.

It actually worked really well in 5.5 when I first wrote it. But when I started, all I had was wireframes. Now that we finally have the design for the site, so I was going to upgrade. Since there have been such improvements (well, besides the bugs) in 5.6 and we didn't have any content or anything to work on, I started in on the theme. Once I had that, I needed to add in data to match what I had in the previous site. I didn't really want to upgrade, so I started over from scratch.

I had installed my package when I started everything again, but didn't check that closely on how it installed. It made the single pages and everything, there were page types and the proper attributes, that means it's all working fine, right?

Not even close.

On install, it creates two new groups for managing the pages. Those groups need to have access to the dashboard sections that allow them to create and edit the pages and custom objects. I noticed that a page no longer being used any more was included today, so I went to fix the install to no longer create it.

That's when I noticed that my code was totally wrong. The permissions were all set up for 5.5 syntax (actually, it's probably older than that) and I needed to update.

No big deal, right? I already had done some research into setting advanced permissions in 5.6 for our Hutman News Add On, so I didn't think it would be too hard.

Updating the page permissions on the dashboard was pretty easy. It's a bit like this:

  1. $dash = Page::getByPath('/dashboard');
  2. $dash->assignPermissions(Group::getByID(GUEST_GROUP_ID), $viewOnly, -1);
  3. $dash->assignPermissions(Group::getByID(REGISTERED_GROUP_ID), $viewOnly, -1);
  4. $dash->assignPermissions(Group::getByID($communityAdministratorsID), $viewOnly);
  5. $dash->assignPermissions(Group::getByID($communitySuperAdministratorsID), $viewOnly);
  6. $dash->assignPermissions(Group::getByID(ADMIN_GROUP_ID), $adminPage);

Honestly, that's a lot better than it was before:

  1. $def = SinglePage::add('/dashboard/communities', $pkg);
  2. $def->update(array('cName' => t('Communities'), 'cDescription' => t('Manage Communities')));
  3.  
  4. $pxml->guests['canRead'] = false;
  5. $pxml->registered['canRead'] = false;
  6. $pxml->group[0]['gID'] = ADMIN_GROUP_ID;
  7. $pxml->group[0]['canRead'] = true;
  8. $pxml->group[0]['canWrite'] = true;
  9. $pxml->group[0]['canApproveVersions'] = true;
  10. $pxml->group[0]['canReadVersions'] = true;
  11. $pxml->group[0]['canDelete'] = true;
  12. $pxml->group[0]['canAdmin'] = true;
  13. $pxml->group[1]['gID'] = $communitySuperAdministratorsID;
  14. $pxml->group[1]['canRead'] = true;
  15. $pxml->group[1]['canWrite'] = true;
  16. $pxml->group[1]['canApproveVersions'] = true;
  17. $pxml->group[1]['canReadVersions'] = true;
  18. $pxml->group[1]['canDelete'] = true;
  19. $pxml->group[1]['canAdmin'] = true;
  20. $pxml->group[2]['gID'] = $communityAdministratorsID;
  21. $pxml->group[2]['canRead'] = true;
  22. $pxml->cInheritPermissionsFrom = "OVERRIDE";
  23. $def->assignPermissionSet($pxml);

Actually Trying To Get Advanced

The thing here was that I needed to do more than just set regular page permissions. I needed to allow only certain types of sub-pages underneath this section. That code wasn't really documented anywhere. In the GUI, it's pretty simple. You add a group to the "Add Sub Page" permission, choose what types they can do and hit save. 

It was pretty simple, if really verbose in 5.5:

  1. $cAID = Group::getByName("Community Administrators")->getGroupID();
  2. $cSAID = Group::getByName("Community Super Administrators")->getGroupID();
  3.  
  4. $perm = array();
  5. // Overall Permissions
  6. $perm['cInheritPermissionsFrom'] = "OVERRIDE";
  7. // subpages inherit permissions from this page
  8. $perm['cOverrideTemplatePermissions'] = 1;
  9. // subpages inherit permissions from page type defaults in dashboard
  10. //$perm['cOverrideTemplatePermissions'] = 1;
  11.  
  12. // edit permissions
  13. $perm['collectionRead'] = array();
  14. $perm['collectionRead'][] = "gID:" . GUEST_GROUP_ID;
  15. $perm['collectionRead'][] = "gID:" . REGISTERED_GROUP_ID;
  16. $perm['collectionRead'][] = "gID:" . ADMIN_GROUP_ID;
  17. $perm['collectionRead'][] = "gID:" . $cAID;
  18. $perm['collectionRead'][] = "gID:" . $cSAID;
  19. $perm['collectionRead'][] = "uID:" . $data['uID'];
  20. $perm['collectionReadVersions'] = array();
  21. $perm['collectionReadVersions'][] = "gID:" . ADMIN_GROUP_ID;
  22. $perm['collectionReadVersions'][] = "gID:" . $cSAID;
  23. $perm['collectionReadVersions'][] = "uID:" . $data['uID'];
  24. $perm['collectionWrite'] = array();
  25. $perm['collectionWrite'][] = "gID:" . ADMIN_GROUP_ID;
  26. $perm['collectionWrite'][] = "gID:" . $cSAID;
  27. $perm['collectionWrite'][] = "uID:" . $data['uID'];
  28. $perm['collectionApprove'] = array();
  29. $perm['collectionApprove'][] = "gID:" . ADMIN_GROUP_ID;
  30. $perm['collectionApprove'][] = "gID:" . $cSAID;
  31. $perm['collectionApprove'][] = "uID:" . $data['uID'];
  32. $perm['collectionDelete'] = array();
  33. $perm['collectionDelete'][] = "gID:" . ADMIN_GROUP_ID;
  34. $perm['collectionDelete'][] = "gID:" . $cSAID;
  35. $perm['collectionAdmin'] = array();
  36. $perm['collectionAdmin'][] = "gID:" . ADMIN_GROUP_ID;
  37. $perm['collectionAdmin'][] = "gID:" . $cSAID;
  38. $perm['collectionAdmin'][] = "uID:" . $data['uID'];
  39.  
  40. // sub page permissions
  41.  
  42. $ccalendarCTID = CollectionType::getByHandle('community_calendar')->getCollectionTypeID();
  43. $cgalleryCTID = CollectionType::getByHandle('community_gallery')->getCollectionTypeID();
  44. $cinfoCTID = CollectionType::getByHandle('community_info')->getCollectionTypeID();
  45. $clocationCTID = CollectionType::getByHandle('community_location')->getCollectionTypeID();
  46. $eventCTID = CollectionType::getByHandle('event_page')->getCollectionTypeID();
  47.  
  48. $perm['collectionAddSubCollection'] = array();
  49. $perm['collectionAddSubCollection'][$ccalendarCTID] = array();
  50. $perm['collectionAddSubCollection'][$ccalendarCTID][] = "gID:" . ADMIN_GROUP_ID;
  51. $perm['collectionAddSubCollection'][$ccalendarCTID][] = "gID:" . $cSAID;
  52. $perm['collectionAddSubCollection'][$ccalendarCTID][] = "uID:" . $data['uID'];
  53.  
  54. $perm['collectionAddSubCollection'][$cgalleryCTID] = array();
  55. $perm['collectionAddSubCollection'][$cgalleryCTID][] = "gID:" . ADMIN_GROUP_ID;
  56. $perm['collectionAddSubCollection'][$cgalleryCTID][] = "gID:" . $cSAID;
  57. $perm['collectionAddSubCollection'][$cgalleryCTID][] = "uID:" . $data['uID'];
  58.  
  59. $perm['collectionAddSubCollection'][$cinfoCTID] = array();
  60. $perm['collectionAddSubCollection'][$cinfoCTID][] = "gID:" . ADMIN_GROUP_ID;
  61. $perm['collectionAddSubCollection'][$cinfoCTID][] = "gID:" . $cSAID;
  62. $perm['collectionAddSubCollection'][$cinfoCTID][] = "uID:" . $data['uID'];
  63.  
  64. $perm['collectionAddSubCollection'][$clocationCTID] = array();
  65. $perm['collectionAddSubCollection'][$clocationCTID][] = "gID:" . ADMIN_GROUP_ID;
  66. $perm['collectionAddSubCollection'][$clocationCTID][] = "gID:" . $cSAID;
  67. $perm['collectionAddSubCollection'][$clocationCTID][] = "uID:" . $data['uID'];
  68.  
  69. $perm['collectionAddSubCollection'][$eventCTID] = array();
  70. $perm['collectionAddSubCollection'][$eventCTID][] = "gID:" . ADMIN_GROUP_ID;
  71. $perm['collectionAddSubCollection'][$eventCTID][] = "gID:" . $cSAID;
  72. $perm['collectionAddSubCollection'][$eventCTID][] = "uID:" . $data['uID'];
  73.  
  74.  
  75. $communityPage->updatePermissions($perm);

It's a lot more involved, but really, it's not complicated. It was a quite simple matter to figure out how to set this up, even though there really wasn't much for documentation at all. Just looking at the $_POST values from the ajax submit gets you a lot of the way there.

5.6 is so amazingly, radically different in how everything works. All of the stuff that happens from the time you open the edit properties dialog involves a ton of different models, tools files, and elements. At one point I had at least 40 files open and was trying to follow how things passed through them and reverse engineer it to figure out where to go for a simple kind of 'shortcut' method to get where I needed to be.

It took me ALL DAMN DAY to figure it out. I had another dev in the #concrete5 room on freenode (Raverix on concrete5.org) try and help me out. He came up with a few working bits of code, but nothing that I could figure out how to modify to do exactly what I wanted to do. And it was assuming a lot of stuff would be hard coded when I needed to know how to get the values properly from  things properly. I don't know where the IDs for several things are coming from, they were part of URL parameters in most of the tools requests, but how the objects fit together to generate those IDs was not straightforward at all. His last solution apparently worked, but with a core modification to make getID() on the Concrete5_Model_PermissionAccess model. I personally don't mind overriding the core, but when making code for clients that don't use our white labeled version and code that will be used in other places (this might very well become another marketplace add on) I don't want to go that route if there's a way around it.

Finding The Answer

I had been going through the files for a long, long time when I finally found what I needed. I'd been tracing stuff, then running searches for function and variable names to find different files that used it. The place that actually had what I was looking for was in the 5.6 upgrade script : {site_root}/concrete/helpers/upgrade/version_560.php

If I had of thought about it more before trying to find the answer, I could have known that the answer would be in this file. Back in the day (before XML/db installations) the site install controller taught me a ton about how to use the API to do stuff. All the pages, attributes, permissions, etc were installed via the API back when I started. There was even a start on custom installs via a menu (long before 'default' and 'empty' and 'whatever you make if you are smart enough to create your own') where the dropdown would control what creation script was included. 

Of course the upgrade script would have it. It has to modify the permissions from the old syntax. While that means some straight-from-the-db stuff and exploding strings containing the permissions, it was 90% of the way there.

These were the two functions that were key:

  1. // This is actually where I looked, but it seems like the next function would be better.
  2. // This function updates permissions for page type masters, I think.
  3.  
  4. protected function migratePagePermissionPageTypes() {
  5. $db = Loader::db();
  6. $tables = $db->MetaTables();
  7. if (!in_array('PagePermissionPageTypes', $tables)) {
  8. return false;
  9. }
  10.  
  11. $r = $db->Execute('select distinct cID from PagePermissionPageTypes order by cID asc');
  12. $pk = PermissionKey::getByHandle('add_subpage');
  13. while ($row = $r->FetchRow()) {
  14. $args = array();
  15. $entities = array();
  16. $ro = $db->Execute('select ctID, uID, gID from PagePermissionPageTypes where cID = ?', array($row['cID']));
  17. while ($row2 = $ro->FetchRow()) {
  18. $pe = $this->migrateAccessEntity($row2);
  19. if (!$pe) {
  20. continue;
  21. }
  22. if (!in_array($pe, $entities)) {
  23. $entities[] = $pe;
  24. }
  25. $args['allowExternalLinksIncluded'][$pe->getAccessEntityID()] = 1;
  26. $args['pageTypesIncluded'][$pe->getAccessEntityID()] = 'C';
  27. $args['ctIDInclude'][$pe->getAccessEntityID()][] = $row2['ctID'];
  28. }
  29. $co = Page::getByID($row['cID']);
  30. if (is_object($co) && (!$co->isError())) {
  31. $pk->setPermissionObject($co);
  32. $pt = $pk->getPermissionAssignmentObject();
  33. $pa = $pk->getPermissionAccessObject();
  34. if (!is_object($pa)) {
  35. $pa = PermissionAccess::create($pk);
  36. }
  37. foreach($entities as $pe) {
  38. $pa->addListItem($pe, false, PagePermissionKey::ACCESS_TYPE_INCLUDE);
  39. }
  40. $pa->save($args);
  41. $pt->assignPermissionAccess($pa);
  42. }
  43. }
  44. }
  45.  
  46. // Even though I didn't use it, this is probably the better function to look at...
  47.  
  48. protected function migratePagePermissions() {
  49. $db = Loader::db();
  50. $tables = $db->MetaTables();
  51. if (!in_array('PagePermissions', $tables)) {
  52. return false;
  53. }
  54. // first, we fix permissions that are set to override but are pointing to another page. They shouldn't do that.
  55. $db->Execute('update Pages set cInheritPermissionsFromCID = cID where cInheritPermissionsFrom = "OVERRIDE"');
  56. // permissions
  57. $waSet = array(
  58. PermissionKey::getByHandle('preview_page_as_user'),
  59. PermissionKey::getByHandle('edit_page_properties'),
  60. PermissionKey::getByHandle('edit_page_contents'),
  61. PermissionKey::getByHandle('move_or_copy_page'),
  62. PermissionKey::getByHandle('add_block_to_area'),
  63. PermissionKey::getByHandle('add_stack_to_area'),
  64. );
  65. if (PERMISSIONS_MODEL == 'simple') {
  66. $waSet[] = PermissionKey::getByHandle('approve_page_versions');
  67. $waSet[] = PermissionKey::getByHandle('delete_page_versions');
  68. $waSet[] = PermissionKey::getByHandle('add_subpage');
  69. }
  70. $permissionMap = array(
  71. 'r' => array(PermissionKey::getByHandle('view_page')),
  72. 'rv' => array(PermissionKey::getByHandle('view_page_versions')),
  73. 'wa' => $waSet,
  74. 'adm' => array(
  75. PermissionKey::getByHandle('edit_page_speed_settings'),
  76. PermissionKey::getByHandle('edit_page_theme'),
  77. PermissionKey::getByHandle('edit_page_type'),
  78. PermissionKey::getByHandle('schedule_page_contents_guest_access'),
  79. PermissionKey::getByHandle('edit_page_permissions')
  80. ),
  81. 'dc' => array(
  82. PermissionKey::getByHandle('delete_page')
  83. ),
  84. 'av' => array(
  85. PermissionKey::getByHandle('approve_page_versions'),
  86. PermissionKey::getByHandle('delete_page_versions')
  87. ),
  88. 'db' => array(PermissionKey::getByHandle('edit_page_contents'))
  89. );
  90.  
  91.  
  92. $r = $db->Execute('select * from PagePermissions order by cID asc');
  93. while ($row = $r->FetchRow()) {
  94. $pe = $this->migrateAccessEntity($row);
  95. if (!$pe) {
  96. continue;
  97. }
  98. $permissions = $this->getPermissionsArray($row['cgPermissions']);
  99. $co = Page::getByID($row['cID']);
  100. foreach($permissions as $p) {
  101. $permissionsToApply = $permissionMap[$p];
  102. foreach($permissionsToApply as $pko) {
  103. $pko->setPermissionObject($co);
  104. $pt = $pko->getPermissionAssignmentObject();
  105. $pa = $pko->getPermissionAccessObject();
  106. if (!is_object($pa)) {
  107. $pa = PermissionAccess::create($pko);
  108. }
  109. $pa->addListItem($pe, false, PagePermissionKey::ACCESS_TYPE_INCLUDE);
  110. $pt->assignPermissionAccess($pa);
  111. }
  112. }
  113. }
  114. }
  115.  
  116. // This was the key part. I really didn't get what the "AccessEntities" were or how to use them
  117. // I didn't even know there were different classes. But I knew I needed them to actually assign
  118. // permissions to the pages. I still don't quite understand them. Wish there was better documentation...
  119.  
  120. protected function migrateAccessEntity($row) {
  121. if ($row['uID'] > 0) {
  122. $ui = UserInfo::getByID($row['uID']);
  123. if ($ui) {
  124. $pe = UserPermissionAccessEntity::getOrCreate($ui);
  125. }
  126. } else {
  127. $g = Group::getByID($row['gID']);
  128. if ($g) {
  129. $pe = GroupPermissionAccessEntity::getOrCreate($g);
  130. }
  131. }
  132. return $pe;
  133. }

I know, I should probably try and get better at figuring things out without code samples, but a lot of stuff like this I do have trouble with. When I see something like the code from the upgrade controller, it's all together, there's not a lot of ambiguity, it's not passing things through multiple files via ajax and elements.

It's really the access entity getOrCreate that was the key. From what I can tell, and I haven't done a lot of testing, it seems like this creates a unique entry for any object that can have permissions. There are several different types you can specify:

  • group
  • user
  • group_set
  • group_combination
  • page_owner
  • file_uploader

I don't know how to use file_uploader or page_owner, but the others seem pretty straightforward. So before you actually assign permissions to an object like a page for one of those groups, you have to create a PermissionAccessEntity object, which keeps a reference to the type that it is. There are also tables for groups, users, etc, that tie back to the PermissionAccessEntities table, which then ties into the PermissionAccessEntitiesTypes table.

So, once you create an entity for a user or group or whatever, that ID is used to tie it to the permissions on any object. So when you add a user to the permission set for a page, you need to pass in the UserInfo object for that user and either get or create a key that can be bound to. Once it exists, it can be used to reference whatever that ID ties to. So it could be permissions for a page, or a file, or whatever, but they would all use the same identifier.

At least, that's my rudimentary understanding of what's happening after spending today trying to reverse engineer it.

It makes sense, but knowing what objects to access was pretty hard to figure out. I'm still not really sure how they're all supposed to fit together, honestly. But I was able to figure out at least this much. I think that there are other models for the ones I'm not using to create the other kinds of objects. After I figured out how to do it, I kind of stopped trying to figure it out.

Next Steps

I think I'm really going to need to do a lot more research into this and a lot of experiments in order to figure out what's actually going on between all these different items. Looking at it, it really does seem like a much better system. If it's correctly understood, you could create custom permissions for custom objects and attach workflows to them so that other people have to approve whatever is done to those objects. There is so much potential power here that it's pretty insane. I'm not sure how it is in other systems, but to me it seems a lot more thought out than what I've seen in the other content management systems that I've used. 

I really wish that there was better documentation on it, though. Really, that's one of the biggest gripes that people have with the system. I learned a whole lot of what I know by simply reading the core and stepping through things that I didn't understand through the debugger. But really, is that a solution for most people? I have people come into the IRC room all the time that are obviously good programmers with a lot of experience doing things that simply can't figure out how to do "X" in c5. I've also seen a lot of code that was sent to me to fix by developers that obviously had been using object oriented PHP for a long time, but had no idea whatsoever how to do simple things. 

Stuff like this, for sure it's more complicated. But it's things that serious developers need to know how to do and that need an easy, defined API for doing in code. Every new version of concrete5 has tons of awesome new features, and a lot of them are really easy to understand. But some of the most important features? They're not documented at all, and at best have a footnote in the version notes. The documentation also doesn't make any reference to what version it's for, so you could be looking at something completely out of date for what you want to do. Most of the core architecture, concepts, etc, are the same. Creating a theme? Exactly the same process as what I did 4 years ago.

But things like how to use advanced permissions as a developer? Nothing. I couldn't even find anyone else wanting to do something like this and asking questions. It makes me wonder if there are many people out there trying to do this kind of stuff, and where they are? 

At any rate, I'm happy that I figured out at least this much. I just wish that it wasn't pretty much an entire day of production trying to figure out what to do for such a simple task. 

blog comments powered by Disqus