Details

    • Epic Name:
      Permissions UI granularity

      Description

      Use case: Toggle UI elements depending on user permissions.

      This is an epic as it touches many areas of the system, what is needed is

      • PHP Repository API possibility to do reverse lookup on permissions
      • Extend REST to add permissions info in response, this should vary by user hash
      • Expose/use this info in REST client(s)
      • Use this info in UI to toggle buttons / actions

      Design:

      Per content item, return hash of content policies accessible within current user rights (hash/context):

      • true (full access)
      • false (no access)
      • hash (some policies gives access under current limitations on UI choices):
        • "owner" (Access if current user is owner): either "item" or "parent"
        • "types" (List of classes, only applicable to content/create like policies where current context is parent)
        • "sections" (List of sections, only applicable to content/create like policies where current context is parent Is this valid? Or should be be evaluated by parent?)
        • "languages" (List of languages, applicable in all cases)

      Simplified editor example on folder items:

      'permissions': {
         'create': {'sections': [1, 2, ...], 'types': ['article', ...]},
         'edit': true,
         'read': true,
         'remove': true,
         'versionread': true,
         ...
      }
      

      If this was full example it would mean user here is missing

      Owner

      While [Parent]Group limitations can safely be evaluate and cached, Parent[Owner] can not, and is thus returned for UI / Client to verify this.
      While we could handle this in API if we could vary cache per user, that might not be very efficient when combined with rest embedding for everything (very low cache hit ratio).

        Issue Links

          Issues in Epic

            Activity

            Hide
            André Rømcke added a comment - - edited

            Possible ways this can look.

            API, something like:

            // Arguments: string $module, ValueObject $object[, array $targets]
            var_dump( $permissionService->getPermissionHash( 'content', $contentObject ) );
             
            // Gives for instance the following where keys reflect the policy "function" names directly
            array(3) {
              ["view"]=>
              bool(true)
              ["edit"]=>
              bool(false)
              ["remove"]=>
              bool(false)
            }
            

            This information could then be more or less directly be embedded in REST response, as long as it is varied by user hash.

            Note: The name "view" and "remove" comes from the policy function names inherited from legacy, in this case "content" module. We can clean it up, but it will break legacy or we will need to map them in legacy storageEngine. It will also break usage of canUser() and hasAccess() API, unless we also spend efforts on mapping it with deprecation notice in Repository as well.

            Show
            André Rømcke added a comment - - edited Possible ways this can look. API, something like: // Arguments: string $module, ValueObject $object[, array $targets] var_dump( $permissionService->getPermissionHash( 'content', $contentObject ) );   // Gives for instance the following where keys reflect the policy "function" names directly array(3) { ["view"]=> bool(true) ["edit"]=> bool(false) ["remove"]=> bool(false) } This information could then be more or less directly be embedded in REST response, as long as it is varied by user hash. Note: The name "view" and "remove" comes from the policy function names inherited from legacy, in this case "content" module. We can clean it up, but it will break legacy or we will need to map them in legacy storageEngine. It will also break usage of canUser() and hasAccess() API, unless we also spend efforts on mapping it with deprecation notice in Repository as well.
            Hide
            Damien Pobel added a comment -

            email from Petar Spanja:

            yesterday I started on a new approach to permission lookup API.

            My initial attempt at https://github.com/ezsystems/ezpublish-kernel/pull/1733 is limited to evaluating the permission for an entity (eg. Content), so it doesn't solve content/create use case. Also it is not generic, but requires special handlers to be implemented. TBH I don't particularly like it, I think it doesn't really belong in the API.

            New approach is based on Andre's work at https://github.com/ezsystems/ezpublish-kernel/compare/EZP-26000-permission-lookup-alt

            This tries to be more generic. The idea is to use policy map settings (ATM hardcoded in RoleService) to define how limitations are handled for permission lookup. For example, a limitation can be marked to return it's values, instead of being evaluated. In case of content/create we have Class, Section and Language marked for return instead of evaluation. The values of these limitations would then be combined by type and delivered over REST.

            However it turns out we can't combine them, because limitations returned could be interdependent. For example, user might have a permission to create an article in section standard and user in section users. Combining those values would destroy information.

            Accounting for that would be rather simple – we could skip combining limitation values and deliver them grouped, which is basically as they are defined by user in the admin UI. The UI application would then need to iterate over them and resolve if something matches or not. From my POV the logic would be the same, but inside a loop – you would need to evaluate a set of permissions separately, instead of evaluating single permission only once.

            That in fact pushes permission evaluation logic from server to client. Considering the alternatives, both my initial attempt and also what was done so far on the REST side, I think this is the way to go.

            The good sides are that permission lookup API would be small and generic and implementation would be simple (in comparison). Once everything is in place, handling new use-cases should be trivial. From the client side POV, while this does shift logic there, instead of doing everything at once, we can start small (with a subset we actually need) and expand as the need arises.

            Before expanding into specifics on how this could look like, I would appreciate some feedback, from you Damien in particular.

            Show
            Damien Pobel added a comment - email from Petar Spanja : yesterday I started on a new approach to permission lookup API. My initial attempt at https://github.com/ezsystems/ezpublish-kernel/pull/1733 is limited to evaluating the permission for an entity (eg. Content), so it doesn't solve content/create use case. Also it is not generic, but requires special handlers to be implemented. TBH I don't particularly like it, I think it doesn't really belong in the API. New approach is based on Andre's work at https://github.com/ezsystems/ezpublish-kernel/compare/EZP-26000-permission-lookup-alt This tries to be more generic. The idea is to use policy map settings (ATM hardcoded in RoleService) to define how limitations are handled for permission lookup. For example, a limitation can be marked to return it's values, instead of being evaluated. In case of content/create we have Class, Section and Language marked for return instead of evaluation. The values of these limitations would then be combined by type and delivered over REST. However it turns out we can't combine them, because limitations returned could be interdependent. For example, user might have a permission to create an article in section standard and user in section users. Combining those values would destroy information. Accounting for that would be rather simple – we could skip combining limitation values and deliver them grouped, which is basically as they are defined by user in the admin UI. The UI application would then need to iterate over them and resolve if something matches or not. From my POV the logic would be the same, but inside a loop – you would need to evaluate a set of permissions separately, instead of evaluating single permission only once. That in fact pushes permission evaluation logic from server to client. Considering the alternatives, both my initial attempt and also what was done so far on the REST side, I think this is the way to go. The good sides are that permission lookup API would be small and generic and implementation would be simple (in comparison). Once everything is in place, handling new use-cases should be trivial. From the client side POV, while this does shift logic there, instead of doing everything at once, we can start small (with a subset we actually need) and expand as the need arises. Before expanding into specifics on how this could look like, I would appreciate some feedback, from you Damien in particular.
            Hide
            Damien Pobel added a comment -

            reply from André Rømcke:

            in simpler terms the discovery so far seems to indicate we won’t be able to return a structure as seen in the Epic:

            'permissions': {
               'create': {‘Class': ['article', …], ’Section’: [3, 5, 8], ..},
               'edit': true,
               'read': true,
               'remove': true,
               'versionread': true,
            }
            

            but rather one the has sets of permissions per policy as limitations has a meaning together:

            'permissions': {
               'create': [{ ‘Class': ['article', …], ’Section’: [3, 5]}, {‘Class': [‘folder'], ’Section’: [8]}, …],
               'edit': true,
               'read': true,
               'remove': true,
               'versionread': true,
            }
            

            So we need to talk on what we should aim for that can provide value for the UI here, otherwise we might end up with something which is difficult to use for UI.

            Show
            Damien Pobel added a comment - reply from André Rømcke : in simpler terms the discovery so far seems to indicate we won’t be able to return a structure as seen in the Epic: 'permissions': { 'create': {‘Class': ['article', …], ’Section’: [3, 5, 8], ..}, 'edit': true, 'read': true, 'remove': true, 'versionread': true, … } but rather one the has sets of permissions per policy as limitations has a meaning together: 'permissions': { 'create': [{ ‘Class': ['article', …], ’Section’: [3, 5]}, {‘Class': [‘folder'], ’Section’: [8]}, …], 'edit': true, 'read': true, 'remove': true, 'versionread': true, … } So we need to talk on what we should aim for that can provide value for the UI here, otherwise we might end up with something which is difficult to use for UI.
            Hide
            Damien Pobel added a comment -

            Petar Spanja André Rømcke
            I'm a bit confused by the example, especially by the reference to Section here since I'm asking for the permissions from a given Content and that Content is assigned to a Section, I would expect the section limitation to interpreted. So in the example above, if I ask for the permissions for a Content in the Section 3, I would expect to have the following response:

            'permissions': {
               'create': [{ ‘Class': ['article', …], ’Section’: [3, 5]}, …], /* no folder since it's allowed only in section 8 */
               'edit': true,
               'read': true,
               'remove': true,
               'versionread': true,
               /* ... */
            }
            

            is that feasible ?

            In case of content/create we have Class, Section and Language marked for return instead of evaluation. The values of these limitations would then be combined by type and delivered over REST.

            but looking at the available limitations in the content/create policy, we have more limitations than those 3:

            • Content Type
            • Language
            • Location
            • ParentClass
            • ParentDepth
            • ParentGroup
            • ParentOwner
            • Section
            • Subtree

            what happens for the others ? Actually while writing this, I realize that most of those could be evaluated server side if you ask for the permissions from a Location. Only Content Type and Language are a user choice. If you ask for the permissions from a Content, then only Section can be evaluated, the rest depends on user's context.

            Show
            Damien Pobel added a comment - Petar Spanja André Rømcke I'm a bit confused by the example, especially by the reference to Section here since I'm asking for the permissions from a given Content and that Content is assigned to a Section, I would expect the section limitation to interpreted. So in the example above, if I ask for the permissions for a Content in the Section 3, I would expect to have the following response: 'permissions': { 'create': [{ ‘Class': ['article', …], ’Section’: [3, 5]}, …], /* no folder since it's allowed only in section 8 */ 'edit': true, 'read': true, 'remove': true, 'versionread': true, /* ... */ } is that feasible ? In case of content/create we have Class, Section and Language marked for return instead of evaluation. The values of these limitations would then be combined by type and delivered over REST. but looking at the available limitations in the content/create policy, we have more limitations than those 3: Content Type Language Location ParentClass ParentDepth ParentGroup ParentOwner Section Subtree what happens for the others ? Actually while writing this, I realize that most of those could be evaluated server side if you ask for the permissions from a Location. Only Content Type and Language are a user choice. If you ask for the permissions from a Content, then only Section can be evaluated, the rest depends on user's context.
            Hide
            André Rømcke added a comment - - edited

            > I'm a bit confused by the example, especially by the reference to Section here since I'm asking for the permissions from a given Content and that Content is assigned to a Section

            content/create (unlike content/edit) refers to a content not created yet, that is why. However assuming this is while browsing the tree, meaning it will get a parent, we can evaluate it on the parent, even if that will then break for custom use cases where user sets section on content creation. ref:

            eZ\Publish\API\Repository\Values\ContentContentCreateStruct

                /**
                 * The section the content is assigned to.
                 * If not set the section of the parent is used or a default section.
                 *
                 * @var mixed
                 */
                public $sectionId;
            

            so given that I don't see how your proposal is feasable, either we simplify and evaluate it on the parent, or we ignore section of parent and always return all as was proposed.

            > Actually while writing this, I realize that most of those could be evaluated server side if you ask for the permissions from a Location.

            exactly, and if you don't provide a location it will be evaluated with context of main location automatically (also for draft using a under the hood spi call to get it)
            And if no location, limitations like Subtree and ParentDepth will simply return false, aka no access to the given policy.

            That said, I think you bring a point here, the fact that a given limitation should return values as opposed to being evaluated actually depends on context that has been provided. So instead of us configuring/hard coding which limitations should return for this api, evaluate should detect it.

            For this we can take advantage of ACCESS_ABSTAIN, currently limited to use on role limitations for when evaluating policies that are not on content module (e.g. a role limited to a subtree, with a section/edit policy assigned to it which has nothing to do with the content subtree), so that can bring us:

            eZ\Publish\SPI\Limitation\Type

                /**
                 * Returned by evaluate if limitation can not determine if use has access.
                 *
                 * Has two meanings:
                 *  Since 5.3.2, If RoleLimitation: Used when provided object is wrong type, implying module can not be limited by the given limitation (e.g. Subtree on section/edit)
                 * Since 6.7, if PolicyLimitation: Used when targets (context) is missing and on permission lookup basically implies possible limitation values should be returned.
                 *
                 * @todo Consider to split this into two different constants, aka introduce new one for new case. naming..
                 */
                const ACCESS_ABSTAIN = null;
                const ACCESS_GRANTED = true; // full access 
                const ACCESS_DENIED = false;// no access
            

            Show
            André Rømcke added a comment - - edited > I'm a bit confused by the example, especially by the reference to Section here since I'm asking for the permissions from a given Content and that Content is assigned to a Section content/create (unlike content/edit) refers to a content not created yet, that is why. However assuming this is while browsing the tree, meaning it will get a parent, we can evaluate it on the parent, even if that will then break for custom use cases where user sets section on content creation. ref: eZ\Publish\API\Repository\Values\ContentContentCreateStruct /** * The section the content is assigned to. * If not set the section of the parent is used or a default section. * * @var mixed */ public $sectionId; so given that I don't see how your proposal is feasable, either we simplify and evaluate it on the parent, or we ignore section of parent and always return all as was proposed. > Actually while writing this, I realize that most of those could be evaluated server side if you ask for the permissions from a Location. exactly, and if you don't provide a location it will be evaluated with context of main location automatically (also for draft using a under the hood spi call to get it) And if no location, limitations like Subtree and ParentDepth will simply return false, aka no access to the given policy. That said, I think you bring a point here, the fact that a given limitation should return values as opposed to being evaluated actually depends on context that has been provided. So instead of us configuring/hard coding which limitations should return for this api, evaluate should detect it. For this we can take advantage of ACCESS_ABSTAIN , currently limited to use on role limitations for when evaluating policies that are not on content module (e.g. a role limited to a subtree, with a section/edit policy assigned to it which has nothing to do with the content subtree) , so that can bring us: eZ\Publish\SPI\Limitation\Type /** * Returned by evaluate if limitation can not determine if use has access. * * Has two meanings: * Since 5.3.2, If RoleLimitation: Used when provided object is wrong type, implying module can not be limited by the given limitation (e.g. Subtree on section/edit) * Since 6.7, if PolicyLimitation: Used when targets (context) is missing and on permission lookup basically implies possible limitation values should be returned. * * @todo Consider to split this into two different constants, aka introduce new one for new case. naming.. */ const ACCESS_ABSTAIN = null; const ACCESS_GRANTED = true; // full access const ACCESS_DENIED = false;// no access
            Hide
            Damien Pobel added a comment -

            so given that I don't see how your proposal is feasable, either we simplify and evaluate it on the parent, or we ignore section of parent and always return all as was proposed.

            I see. I was confused because in PlatformUI, you can not choose in which section the Content will be created, the section is automatically inherited from the parent Content. And indeed, when thinking outside of PlatformUI, it makes sense to have the Section list in the output (even if it will be more work in UI )

            Also while re-reading the example above, I'm not sure whether it is supposed to be the return value of a Public API thing or of the REST API. If it's the later, we should not use the Content Type identifier and the Section id but the Content Type REST id and Section REST id (also in both cases, Class should be replaced by ContentType to be less legacyish )

            Other than that, that seems ok to me, at least this seems to be something we can use.

            Show
            Damien Pobel added a comment - so given that I don't see how your proposal is feasable, either we simplify and evaluate it on the parent, or we ignore section of parent and always return all as was proposed. I see. I was confused because in PlatformUI, you can not choose in which section the Content will be created, the section is automatically inherited from the parent Content. And indeed, when thinking outside of PlatformUI, it makes sense to have the Section list in the output (even if it will be more work in UI ) Also while re-reading the example above, I'm not sure whether it is supposed to be the return value of a Public API thing or of the REST API. If it's the later, we should not use the Content Type identifier and the Section id but the Content Type REST id and Section REST id (also in both cases, Class should be replaced by ContentType to be less legacyish ) Other than that, that seems ok to me, at least this seems to be something we can use.
            Hide
            André Rømcke added a comment - - edited

            > Also while re-reading the example above, I'm not sure whether it is supposed to be the return value of a Public API thing or of the REST API.

            It's Pseudo code for public API, so don't take it to literally yet

            As for "Class", I'm torn on this, one one side yes we should use new terms, however that will then diverge from data returned from RoleService, including from REST, meaning it can not be mapped, as these are the identifiers of the limitations, as inherited by legacy. Possible alternative, which can somewhat make sense on Public API side at least, would be to start to use the class name instead, which is ContentTypeLimitation in this case.

            Show
            André Rømcke added a comment - - edited > Also while re-reading the example above, I'm not sure whether it is supposed to be the return value of a Public API thing or of the REST API. It's Pseudo code for public API, so don't take it to literally yet As for "Class", I'm torn on this, one one side yes we should use new terms, however that will then diverge from data returned from RoleService, including from REST , meaning it can not be mapped, as these are the identifiers of the limitations, as inherited by legacy. Possible alternative, which can somewhat make sense on Public API side at least, would be to start to use the class name instead, which is ContentTypeLimitation in this case.

              People

              • Assignee:
                Unassigned
                Reporter:
                André Rømcke
              • Votes:
                1 Vote for this issue
                Watchers:
                7 Start watching this issue

                Dates

                • Created:
                  Updated: