Please upgrade here. These earlier versions are no longer being updated and have security issues.
HackerOne users: Testing against this community violates our program's Terms of Service and will result in your bounty being denied.

Is it possible to set plugins permissions on a per Category basis?

businessdadbusinessdad Stealth contributor MVP
edited March 2013 in Vanilla 2.0 - 2.8

Ref: Vanilla 2.0.8.14

I'm using a couple of plugins that register some permissions (e.g. Plugins.MyPlugin.CanDoSomething), and I wanted to enable/disable some of them on some Categories. However, when I click on "This category has custom permissions", I only see the system ones, i.e. the ones for Comments and Discussions. No plugin-specific permission is listed.

After some debugging, I noticed that the permissions are registered and retrieved correctly when the Category page is rendered, but then they are filtered out in PermissionModel::GetJunctionPermissions(). The line that filters them out is the following:

foreach($Row as $PermissionName => $Value) {
  if(!($Value & 2))
    continue; // permission not applicable to this junction table
    //[...]

I don't know what "2" means, so I went on searching, to see if I could find a clue. All I could find, apparently related to the magic number, was PermissionModel::Define():

foreach($PermissionNames as $Key => $Value) {
            if(is_numeric($Key)) {
                $PermissionName = $Value;
            $DefaultPermissions[$PermissionName] = 2;
            } else {
                $PermissionName = $Key;

            if ($Value === 0)
               $DefaultPermissions[$PermissionName] = 2;
            elseif ($Value === 1)
               $DefaultPermissions[$PermissionName] = 3;
            elseif (!$Structure->ColumnExists($Value) && in_array($Value, $PermissionNames))
               $DefaultPermissions[$PermissionName] = $PermissionNames[$Value] ? 3 : 2;
            else
               $DefaultPermissions[$PermissionName] = "`{$Value}`"; // default to another field
         }
// [...]

I don't know what these magic numbers 0, 1, 2 and 3 mean, nor I could figure out why they are assigned to some permissions, therefore I gave up searching (a grep for "2" gives way too many results). My question, though, still stands: is it possible to tell Vanilla that I would like some plugin permissions to be enabled/disabled for each Category?

Thanks in advance for the help.

Answers

  • hgtonighthgtonight ∞ · New Moderator

    While I can't tell you for certain if you can achieve this through the framework code, it looks like the plugin permissions can be set per category. I am just inferring this from the mysql db permissions table.

    As far as the magic number, it looks like they are using numerals and bitmasking for the actual permissions. Think of something like file permissions using chmod 700 that means the user can read (1) + write (2) + execute (3) = 7. This lets you check an individual permission by just bitwise ANDing it (if(!($Value & 2))). Keep in mind this is mostly speculation and inferences.

    Search first

    Check out the Documentation! We are always looking for new content and pull requests.

    Click on insightful, awesome, and funny reactions to thank community volunteers for their valuable posts.

  • businessdadbusinessdad Stealth contributor MVP

    @hgtonight said:
    While I can't tell you for certain if you can achieve this through the framework code, it looks like the plugin permissions can be set per category. I am just inferring this from the mysql db permissions table.

    I also thought that by looking at the table, apparently there is no real difference between the system permissions and the plugin ones, apart that the former get set to "2" and, therefore, picked up by the PluginModel.

    As far as the magic number, it looks like they are using numerals and bitmasking for the actual permissions. Think of something like file permissions using chmod 700 that means the user can read (1) + write (2) + execute (3) = 7. This lets you check an individual permission by just bitwise ANDing it (if(!($Value & 2))). Keep in mind this is mostly speculation and inferences.

    That would explain their usage. Now it would be interesting to find out what these bit masks mean. It seems I will have to go deeper, "down into the mines of Moria".

  • peregrineperegrine MVP
    edited March 2013

    if($this->CheckPermission(Plugins.MyPlugin.variable, FALSE, 'Category', $JunctionID)) {

    The JunctionID associated with $Permission (ie. A discussion category identifier).

    use category or create your own junction table.

    permission 3 never gets filtered out.

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • businessdadbusinessdad Stealth contributor MVP

    @peregrine said:
    if($this->CheckPermission(Plugins.MyPlugin.variable, FALSE, 'Category', $JunctionID)) {

    Thanks for the reply. This how I'm checking it now, but the issue is that the permission cannot be set on a per-category basis. That is, it's global, "all categories or none". If you open a Category and click on "this category has custom permissions", you will see that none of the Plugins permissions appear in the page. In my case, the permissions must not be global, they change from category to category.

    Let's make a simple example: I want to prevent users from posting a Discussion in Category A, but they can post a Question. Vice-versa, they can post a Discussion in Category B, but not a question. A new plugin could register the following permissions:

    • Plugins.MyPlugin.CanPostDiscussion
    • Plugins.MyPlugin.CanPostQuestion

    Then, the plugin will check, before allowing to post:

    if("Post is a Question" && $this->CheckPermission(Plugins.MyPlugin.CanPostQuestion, FALSE, 'Category', $CategoryID)) {
      // Allow posting
    }
    

    The issue is that CanPostDiscussion and CanPostQuestion will not appear in Category permissions, only in the Global ones. Therefore, before the User posts something, the check will always pass, or always fail, depending on the global setting and regardless of the Category.

  • peregrineperegrine MVP
    edited March 2013

    if all else fails.

    create a config setting for allowed categories in an array via category id.

    $CategoryId = GetValue('CategoryID',$Sender->Discussion);

    multiples
    if ( ((in_array($CaegoryID, $AllowedCategoryArray)) && (($this->CheckPermission(Plugins.MyPlugin.CanPostQuestion))) {

    add a settable multiple category setting in dashboard that writes to a config statement.

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • businessdadbusinessdad Stealth contributor MVP

    @peregrine said:
    if all else fails.
    create a config setting for allowed categories in an array via category id.
    add a settable multiple category setting in dashboard that writes to a config statement.

    That would be a partial workaround, but I forgot to mention something important (stupid me): the settings have to be per Category, and per Role. Moderators may post Questions and Discussions in Category A, Users can only post Questions in Category A, Administrators can post anything.

    It's fairly complex, that's why I hoped I could to leverage the existing Permissions model, which already implements role permissions checks.

  • peregrineperegrine MVP
    edited March 2013

    yea.

    I asked about permissions here when i first joined the forum ...

    http://vanillaforums.org/discussion/19084/could-any-developers-shed-some-light-on-checkpermissions#latest

    not many responses to that question

    Unless you get a better answer - a combo of custom categories to control posting and checking category for plugin use. 3 conditionals per.

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • Permissions are agnostic, they have no awareness of the plugin other then what you put in your logic.

    You can't do it through the plugin info RegiterPermisions becuase it can't supply all the parameter currently. If you do it explicitly it probably a lot clearer anyway. You could try:

     $PermissionModel = Gdn::PermissionModel();
     $PermissionModel->Define(
             array(
              'Plugins.MyPlugin.CanPostDiscussion' => 1,
              'Plugins.MyPlugin.CanPostQuestion' => 1
             ),
         'tinyint',
         'Category',
         'PermissionCategoryID'
     );
    

    You could do this this in Setup.

    The relevant section is the bit with JunctionTable. I believe that 1 gives you a default of on.

    There are some gems of information in vanilla/dashboard's settings/structure.php. I recommend that you have a look.

    grep is your friend.

  • businessdadbusinessdad Stealth contributor MVP

    @x00 said:
    Permissions are agnostic, they have no awareness of the plugin other then what you put in your logic.

    You can't do it through the plugin info RegiterPermisions becuase it can't supply all the parameter currently. If you do it explicitly it probably a lot clearer anyway. You could try:

     $PermissionModel = Gdn::PermissionModel();
     $PermissionModel->Define(
             array(
            'Plugins.MyPlugin.CanPostDiscussion' => 1,
            'Plugins.MyPlugin.CanPostQuestion' => 1
             ),
       'tinyint',
       'Category',
       'PermissionCategoryID'
     );
    

    Ah, finally! PermissionModel::Define(), that was the method I was looking for! I went through the PermissionModel, but I must have missed it. That's an excellent starting point, and I will have a look at structure.php as well.

  • how can you have missed it? Look at your previous posts, you referenced it lol

    grep is your friend.

  • businessdadbusinessdad Stealth contributor MVP

    @x00 said:
    how can you have missed it? Look at your previous posts, you referenced it lol

    That's true! I can tell you how I missed it. It was a combination of the following:

    • Sleepiness. It was very late night (actually, early morning) when I tried to tackle the problem.
    • Missing the forest for the trees. I was looking inside the Define() method, but I didn't look how it was used, and I got lost in trying to figure out what the magic numbers were doing.
    • Plain and simple "searching for the glasses that I have on my head".

    The important thing is that I now have a lead, thanks to your suggestion. Besides, I learned that structure.php also contains valuable information. At the end, despite the silliness, the outcome is positive. :)

  • businessdadbusinessdad Stealth contributor MVP

    Update: I used PermissionModel::Define() to define the new permissions, specifying Category as the junction table as PermissionCategoryID as the junction column. Apart from some values changed in Gdn_Permissions table, which I speculate will come into play during a call to CheckPermission(), nothing else has changed.
    Next step will be finding out how to display those permissions in the Category Permissions page (there's little point in having per-category permissions, if they cannot be changed without opening the table).

    It will work, eventually. :)

  • businessdadbusinessdad Stealth contributor MVP

    I think I found out what's happening, and I think it's due to a bug. Here's the (long) story.

    When a new Permission is registered by a plugin, its name is, usually, Plugins.SomePlugin.SomePermission. By using the following code, thanks to the suggestion given by @x00, I was able make them pass the first test (i.e. if(!($Value & 2))):

    Gdn::PermissionModel()->Define(array(
    'Plugins.MyPlugin.CanPostQuestion',
    'Plugins.MyPlugin.CanPostDiscussion'),
    'tinyint',
    'Category',
    'PermissionCategoryID');

    This, however, is not enough. The RegisterPermissions section of the plugin declaration still has to be present (the reason for this is below). So, I also added that:

    'RegisterPermissions' => array(
      'Plugins.MyPlugin.CanPostQuestion',
      'Plugins.MyPlugin.CanPostDiscussion')
    

    Despite the permissions passing the above test, they were still not appearing in the Categories Permission page. I checked the code again, and I found what I think is a bug. The statement that excluded them is in PermissionModel::GetJunctionPermissions():

    $Namespaces = $this->GetAllowedPermissionNamespaces();
    // Other stuff...
    if($index = strpos($PermissionName, '.')) {
      // Here is the issue, this in_array() fails
      if(!in_array(substr($PermissionName, 0, $index), $Namespaces))
        continue; // permission not in allowed namespaces
    }
    

    The $Namespaces array contained the following (amongst others):

    • Vanilla
    • Garden
    • Plugins.MyPlugin
    • Plugins.MyOtherPlugin
      [...]

    The Plugins.xyz namespaces are added automatically, by PermissionModel::GetAllowedPermissionNamespaces(), which parses the RegisterPermissions attribute in plugin's declaration (that's why it still has to exist, even if Gdn::PermissionModel()->Define() is called explicitly). The namespace is extracted using a strrpos(), which cuts away everything until the rightmost dot.

    The issue is that the check extracts the permission namespace using strpos() and stopping at the leftmost dot. That is, the value comes out as just Plugins, which doesn't exist in the list, thus excluding the permission as "not in allowed namespaces".

    I then started wondering why the discrepancy in the logic and, most importantly, why the permissions show correctly in the global page. I debugged that page, and there I found the answer. Method PermissionModel::GetGlobalPermissions() performs almost the same check, but with an important difference:

    if(!in_array(substr($PermissionName, 0, $index), $Namespaces) &&
       !in_array(substr($PermissionName, 0, strrpos($PermissionName, '.')), $Namespaces))
    

    That is, it checks it twice:

    • First check extracts Plugins from the Permission name, like the one above, and fails.
    • Second check extracts Plugins.MyPlugin from the Permission name, like the one above, and passes.

    I then modified PermissionModel::GetJunctionPermissions() in Core, and, finally, the Plugins Permissions show in both the Category Permissions and the Global Permissions pages. They also seem to be processed correctly by Gdn_Session::CheckPermission().

    Solutions

    There are two solutions for the issue (actually, one is a workaround):
    1. Solution: fix the check in PermissionModel::GetJunctionPermissions() by adding the second clause, as described above. This is the solution I adopted at the moment.
    2. Workaround: use a single dot in the permission name (e.g. declare it as just "MyPlugin.SomePermission"). This is not standard, but it will trick the PermissionModel into extracting the same namespace, since there is only one dot. One of the side effects of this workaround is that the permissions will be rendered in their own separate table in the Permission pages.

    The same discrepancy exists in 2.0.x, 2.1a and 2.1b. I'm now wondering if I should report this as a bug, or if the original namespace check was like that by design.

  • hgtonighthgtonight ∞ · New Moderator

    This definitely smells of a bug. Please file it on github.

    Also, thanks for the insight!

    Search first

    Check out the Documentation! We are always looking for new content and pull requests.

    Click on insightful, awesome, and funny reactions to thank community volunteers for their valuable posts.

  • businessdadbusinessdad Stealth contributor MVP

    Filed as issue 1541.

Sign In or Register to comment.