[keycloak-user] Question about CBAC + Pushing Claims + Authorization Scopes

Pedro Igor Silva psilva at redhat.com
Thu Aug 1 11:16:20 EDT 2019


Sorry for the late reply.

First of all, thanks for hacking the SPI. Feedback is always important.

+1 for a PR. If you create a JIRA and provide all the details and
requirements you gave me here, I'm able to review as soon as you have
something.

This could maybe be as simple as a switch on the scope-based permission ?
Anyway, looking forward to see your work :)

Thanks.

On Tue, Jul 30, 2019 at 7:34 AM Álvaro Gómez <
alvaro.gomez.gimenez at tecsisa.com> wrote:

> Hi Pedro,
>
> Sorry for the delay, we've been implementing some POCs in order to
> experiment with the tools you mentioned (add & remove claims from
> policies). Answering your questions:
>
> <<Could you share how the "Is-Product-Manager" looks like?>>
>
> Our role polices are based in client-roles (denormalized by tenant using
> '@'). E.g.:
>
>  * Product-Manager at Organization-1
>  * Product-Manager at Organization-2
>  * Is-Seller at Organization-1
>  * Is-Seller at Organization-2
>
> We implement our custom policies in Scala using SPIs but we made an
> example of the "Is-Product-Manager" policy in Javascript to show you it's
> basic behavior:
>
>     var roleName = 'admin';
>     var clientId = 'test-client';
>     var tenants =  $evaluation.getPermission().getClaims().get("tenants");
>
>     var hasTenantRole = true;
>     for (var i in tenants) {
>         hasTenantRole &=
>             $evaluation
>                 .getContext()
>                 .getIdentity()
>                 .hasClientRole(clientId, roleName + '@' + tenants[i]);
>     }
>
>     if(hasTenantRole) {
>         $evaluation.grant();
>     } else {
>         $evaluation.deny();
>     }
>
> The policy checks the claim "tenants" and evaluates if the requesting
> identity has all the corresponding client roles (role @ tenant). This
> policy works well with non-scoped resources but, as we exposed in previous
> mails, it won't work with scoped resources (since the claims are more
> complex). To support scoped-resources (with the scope-aware claim
> structure) we could use policies which only check tenants for an specific
> scope. E.g.:
>
>     var roleName = 'admin';
>     var clientId = 'test-client';
>     var scope =    'sell';
>     var scopesByTenant =  $evaluation.getPermission().getClaims();
>
>     var hasTenantRole = true;
>     for (var tenant in scopesByTenant) {
>         if(scopesByTenant[tenant].contains(scope)) {
>             hasTenantRole &=
>                 $evaluation
>                     .getContext()
>                     .getIdentity()
>                     .hasClientRole(clientId, roleName + '@' + tenant);
>         }
>     }
>
>     if(hasTenantRole) {
>         $evaluation.grant();
>     } else {
>         $evaluation.deny();
>     }
>
>
>
> <<Based on your description, the permission looks correct given that only
> "read" was granted but not "update" for org-2. So if you try to update a
> resource in org-2 you should be blocked. I guess your point is that
> "update" is still as a claim for org-2 ?>>
>
>
> Yes, our problem is that Keycloak does not know our claim structure so it
> can't remove denied scopes. We tried to manipulate the claims inside the
> policies (using add and remove claim mechanisms) but we didn't find an
> algorithm that suits our needs. We can't remove our scope claims inside a
> Policy since the overall scope granting decision is made by the
> Scope-Permission. That means that, inside a Policy, you don't have enough
> information to remove our scope claims since they may be granted by further
> policies (assuming the scope-permissions involve multiple policies with an
> affirmative decision strategy).
>
> Our best approach to clean denied scopes from our custom claim structure
> is to override the default Scope-Permission behavior. We've noticed that
> Scope-Permissions are, in fact, implemented as Policies so we can deploy an
> SPI overriding the ScopePolicyProvider (We tried to create a new type of
> permission instead of overriding it but Keycloak seems to assume that only
> resource and scope policy-permissions exist). We added a piece of code to
> remove denied scopes and the OverridedScopePolicyProvider looks as follows:
>
>     public class OverridedScopePolicyProviderJava extends
> ScopePolicyProvider {
>         @Override
>         public void evaluate(Evaluation evaluation) {
>             super.evaluate(evaluation);
>
>             DefaultEvaluation defaultEvaluation =
> DefaultEvaluation.class.cast(evaluation);
>             if(defaultEvaluation.getEffect() == Decision.Effect.DENY) {
>                 evaluation
>                     .getPermission()
>                     .getClaims()
>                     .entrySet()
>                     .stream()
>                     .collect(
>                         Collectors.toMap(
>                             Map.Entry::getKey,
>                             e ->
>                                 e.getValue()
>                                 .removeAll(
>                                     defaultEvaluation
>                                         .getParentPolicy()
>                                         .getScopes()
>                                         .stream()
>                                         .map(s -> s.getName()
>                                 ).collect(Collectors.toSet())
>                             )
>                         )
>                     );
>             }
>         }
>     }
>
> We know this is a bit tricky since, even though scope and resource
> permissions are implemented as policies, we know they are special ones. Are
> we going too far with SPIs? Is there a better way to solve this scenario?
> Maybe Keycloak should allow scope-defined claims in permissions? If any
> changes were needed in Keycloak to support this we could help with the PR.
>
> Regards,
> Álvaro.
>
> El jue., 25 jul. 2019 a las 23:19, Pedro Igor Silva (<psilva at redhat.com>)
> escribió:
>
>> On Thu, Jul 25, 2019 at 10:13 AM Álvaro Gómez <
>> alvaro.gomez.gimenez at tecsisa.com> wrote:
>>
>>>
>>> 1.- The policy "Is-Product-Manager", involved in the permission
>>> "product:update", should only check if the Requesting Party is
>>> "Product-Manager" in the context of the tenants which contains "update" in
>>> the values of the claims. In the example, the policy "Is-Product-Manager",
>>> when evaluated for the permission "product:update", should only check if
>>> the Requesting Party is "Product-Manager" in the context of
>>> "Organization-2" since it makes no sense to check if it's "Product-Manager"
>>> in "Organization-1" (The "update" scope was only requested for
>>> "Organization-2"). This could be solved by creating a policy
>>> "Is-Product-Manager:update" (which only checks tenants associated to the
>>> scope "update" in the pushing claims) but we found this solution a bit
>>> tricky.
>>>
>>
>> Could you share how the "Is-Product-Manager" looks like?
>>
>>
>>> 2.- If a Requesting Party requested access to the scope "update" along
>>> with other scope "read" in the context of "Organization-2" without being
>>> "Product-Manger" in such organization (which implies it can "read" but not
>>> "update" a product in that organization), we would end up with the
>>> following permission (The first time you request an RPT, Keycloak returns
>>> the granted scopes even though they are not the whole requested scopes):
>>>
>>> {
>>>   "resource": "product",
>>>   "resource_scopes": ["read"],
>>>   "claims": {
>>>     "Organization-2": ["update", "read"]
>>>   }
>>> }
>>>
>>
>>> As we can see, the granted scope is only "read" but the pushed claims in
>>> "Organization-2" are both "update" and "read" (We requested both scopes in
>>> the ticket). The claim becomes inconsistent since the scope "update" should
>>> be removed and keycloak is not able to do so since it does not understand
>>> our custom claims. If claims were natively grouped by scopes, Keycloak
>>> would clean claims from not-granted scopes. Wdyt?
>>>
>>
>> Based on your description, the permission looks correct given that only
>> "read" was granted but not "update" for org-2. So if you try to update a
>> resource in org-2 you should be blocked. I guess your point is that
>> "update" is still as a claim for org-2 ?
>>
>> FYI, from JS policies you should be able to remove/add claims from
>> permissions so that you have more control over what is sent back to your
>> application. As well as, push back claims so that you can advertise actions
>> or anything else that the resource server should do before granting access
>> to a resource. Maybe this can be an alternative.
>>
>>>
>>> Thanks!
>>> Álvaro.
>>>
>>> El jue., 25 jul. 2019 a las 14:16, Pedro Igor Silva (<psilva at redhat.com>)
>>> escribió:
>>>
>>>> Considering you are in control on how the ticket is created and how
>>>> claims are set on it, would be an option to use a specific claim for each
>>>> tenant so that in your policies you check tenants based on the claim's key ?
>>>>
>>>> On Thu, Jul 25, 2019 at 9:12 AM Álvaro Gómez <
>>>> alvaro.gomez.gimenez at tecsisa.com> wrote:
>>>>
>>>>> Hi Pedro,
>>>>>
>>>>> We are performing HTTP ticket requests from our application (An API
>>>>> acting as a Resource Server). As an example, having the following endpoint:
>>>>>
>>>>> GET /api/tenants/__TENANT_ID__/products/__PRODUCT_ID__
>>>>>
>>>>> If we use non-scoped resources (In order to simplify the example) the
>>>>> API behaves as follows:
>>>>>
>>>>>   ** The Requesting Party performs this action:
>>>>>
>>>>>      GET /api/tenants/Organization-1/products/Product-X
>>>>>
>>>>>   1.- If there is no "permissions" claim (Or it does not contain the
>>>>> required authorization info, described in step 2) the API performs a ticket
>>>>> request for the resource "Product-X" pushing the tenant "Organization-1" in
>>>>> a claim:
>>>>>
>>>>>     POST
>>>>> https://localhost:8080/auth/realms/***/authz/protection/permission
>>>>>        [{
>>>>>             "resource_id": "Product-X",
>>>>>             "claims": { "tenant": [ "Organization-1" ] }
>>>>>        }]
>>>>>
>>>>>   The Requesting Party uses the ticket to obtain a valid RPT
>>>>> containing the following authorization info:
>>>>>
>>>>>       "permissions": [
>>>>>         {
>>>>>             "resource_id": "Product-X",
>>>>>             "claims": { "tenant" : ["Organization-1"] }
>>>>>         }
>>>>>       ]
>>>>>
>>>>>   ** The Requesting Party performs the following action using the
>>>>> previously obtained RPT:
>>>>>
>>>>>      GET /api/tenants/Organization-2/product/Product-X
>>>>>
>>>>>   2.- The API checks if the specified resource "Product-X" exists in
>>>>> the RPT "permissions" claim and contains "Organization-2" in the "tenant"
>>>>> pushed claim. Since the resource "Product-X" is only provided for the
>>>>> context "Organization-1" the API requests a ticket for the resource
>>>>> "Product-X" in the context of the tenant "Organization-2".
>>>>>
>>>>>    POST
>>>>> https://localhost:8080/auth/realms/***/authz/protection/permission
>>>>>         [{
>>>>>             "resource_id": "Product-X",
>>>>>             "claims": { "tenant": [ "Organization-2" ] }
>>>>>         }]
>>>>>
>>>>>     The Requesting Party uses the ticket to upgrade the previous RPT.
>>>>> The upgraded RPT now contains both tenants in the pushed claims:
>>>>>
>>>>>         "permissions": [
>>>>>             {
>>>>>                 "resource_id": "Product-X",
>>>>>                 "claims": { "tenant" : ["Organization-1",
>>>>> "Organization-2"] }
>>>>>             }
>>>>>         ]
>>>>>
>>>>> This works great with non-scoped resources since, for now on, the
>>>>> Resource server can grant access to "Product-X" in both contexts
>>>>> "Organization-1" and "Organization-2". Also, the Resource Server will
>>>>> obtain new tickets if new contexts (tenants) are requested. However, when
>>>>> we use scoped-resources, since the pushing claims are not specific to the
>>>>> scopes being requested, the Resource Server could not determine if the
>>>>> combination of "Product-X" and some scope is defined for an specific
>>>>> tenant. We could support this use-case removing scopes from the equation
>>>>> and creating non-scoped resources like "Product-X:read", "Product-X:write",
>>>>> etc. However, while we think that this should be implemented using scopes
>>>>> instead of non-scoped resources, we don't know how to manage claims as we
>>>>> discussed in the first mail.
>>>>>
>>>>> Regards,
>>>>> Álvaro.
>>>>>
>>>>> El mié., 24 jul. 2019 a las 22:34, Pedro Igor Silva (<
>>>>> psilva at redhat.com>) escribió:
>>>>>
>>>>>> Hi Álvaro,
>>>>>>
>>>>>> You are not missing anything and that is how claims are handled. They
>>>>>> are a permission-level (resource + scopes) info and not specific to only
>>>>>> the scopes being requested/granted.
>>>>>>
>>>>>> Before finding alternatives, could you tell me how are you pushing
>>>>>> these claims? Are you using our adapters or manually performing HTTP
>>>>>> requests from your app?
>>>>>>
>>>>>> Regards.
>>>>>>
>>>>>> On Wed, Jul 24, 2019 at 10:20 AM Álvaro Gómez <
>>>>>> alvaro.gomez.gimenez at tecsisa.com> wrote:
>>>>>>
>>>>>>> Hi,
>>>>>>>
>>>>>>> We are applying RBAC and CBAC models to evaluate permissions in a
>>>>>>> multi-tenant UMA application. We are using Pushing Claims to let
>>>>>>> custom
>>>>>>> policies determine if an user has an specific role in a provided
>>>>>>> context
>>>>>>> (tenant) via Pushing Claims.
>>>>>>>
>>>>>>> Everything works fine if we use non-scoped resources but things get
>>>>>>> a bit
>>>>>>> confusing when we use scoped ones since the pushing-claims
>>>>>>> (representing
>>>>>>> the tenants) end up mixed in the RPT permission claim without
>>>>>>> leaving any
>>>>>>> trace of the scopes with which they were pushed along. Consider the
>>>>>>> following example:
>>>>>>>
>>>>>>> We have an application which manages products (represented by
>>>>>>> resources).
>>>>>>> There are profiles (represented by roles) which allow users to sell,
>>>>>>> modify
>>>>>>> or delete products (represented by scopes). A certain user may
>>>>>>> interact
>>>>>>> with one product in the context of a tenant (Determined by the
>>>>>>> Pushing
>>>>>>> claim) with an specific role and with some different role from other
>>>>>>> tenant.
>>>>>>>
>>>>>>> - Resource:
>>>>>>>   * product (With scopes sell and update)
>>>>>>>
>>>>>>> - Roles:
>>>>>>>   * Seller
>>>>>>>   * Product-Manager
>>>>>>>
>>>>>>> - Policies:
>>>>>>>   * Is-Seller (In the Tenant specified in the Pushing Claim "tenant")
>>>>>>>   * Is-Product-Manager (In the Tenant specified in the Pushing Claim
>>>>>>> "tenant")
>>>>>>>
>>>>>>> - Permissions:
>>>>>>>   * product:sell -> Provides the "sell" scope of the resource
>>>>>>> "product" if
>>>>>>> the "Is-Seller" policy evaluates to grant.
>>>>>>>   * product:update -> Provides the "update" scope of the resource
>>>>>>> "product" if the "Is-Product-Manager" policy evaluates to grant.
>>>>>>>
>>>>>>> - Users:
>>>>>>>   * Alice -> Alice is "Seller" in the tenant "Organization-1" and is
>>>>>>> "Product-Manager" in the tenant "Organization 2" so she should be
>>>>>>> able to
>>>>>>> sell products in the context of the tenant "Organization-1" and
>>>>>>> update
>>>>>>> products in the context of "Organization-2" but neither "update"
>>>>>>> products
>>>>>>> in the context of "Organization-1" or sell products in the context of
>>>>>>> "Organization-2".
>>>>>>>
>>>>>>> 1.- Alice requests an RPT using the following ticket:
>>>>>>>      { "resource": "product", "resource_scopes": ["sell"], "claims":
>>>>>>> {
>>>>>>> "tenant": ["Organization-1"] } }
>>>>>>>
>>>>>>>     Since Alice is "Seller" in the "Organization-1" (meaning the
>>>>>>> Policy
>>>>>>> "Is-Seller" will evaluate to "grant" if the provided claim value is
>>>>>>> "Organization-1" and the evaluated Identity is Alice) an RPT is
>>>>>>> emitted
>>>>>>> with the following "permission" claim:
>>>>>>>
>>>>>>>     [{
>>>>>>>        "resource": "product",
>>>>>>>        "resource_scopes": ["sell"],
>>>>>>>        "claims": { "tenant": ["Organization-1"] }
>>>>>>>     }]
>>>>>>>
>>>>>>> 2.- Alice upgrades the previous RPT with the following ticket:
>>>>>>>      { "resource": "product", "resource_scopes": ["update"],
>>>>>>> "claims": {
>>>>>>> "tenant": ["Organization-2"] } }
>>>>>>>
>>>>>>>    Here is were things get confusing to us. We'd expect Alice to be
>>>>>>> granted
>>>>>>> when requesting the scope "update" in the context of
>>>>>>> "Organization-2" since
>>>>>>> Alice has the role "Product-Manager" in that tenant. That would be
>>>>>>> what
>>>>>>> happened if Alice was requesting the RPT for the first time instead
>>>>>>> of
>>>>>>> upgrading a previous one. However, since we are upgrading the RPT
>>>>>>> obtained
>>>>>>> in Step 1, when the policy "Is-Product-Manager" is evaluated, the
>>>>>>> claim
>>>>>>> "tenant" is mixed with the one in Step 1 (Since they are not grouped
>>>>>>> by
>>>>>>> scope) resulting in the following permission:
>>>>>>>
>>>>>>>    {
>>>>>>>       "resource": "product",
>>>>>>>       "resource_scopes": ["sell", "update"],
>>>>>>>       "claims": {
>>>>>>>           "tenant": ["Organization-1", "Organization-2"]
>>>>>>>        }
>>>>>>>    }
>>>>>>>
>>>>>>>   The policy can't evaluate to grant since Alice is not
>>>>>>> "Product-Manager"
>>>>>>> in both tenants "Organization-1" and "Organization-2" (Obtained
>>>>>>> through
>>>>>>> $evaluation.getPermission().getClaims()). When evaluating this
>>>>>>> policy we
>>>>>>> would only be interested in the pushing-claim `{ "tenant":
>>>>>>> ["Organization-2"] }` which was pushed along with the scope "update"
>>>>>>> (which
>>>>>>> is the one being evaluated by the permission "product:update"
>>>>>>> associated
>>>>>>> with this Policy).
>>>>>>>
>>>>>>>    Shouldn't the claims be grouped by the scopes which with they were
>>>>>>> pushed along? (See example at the end of this text), Are we missing
>>>>>>> something?
>>>>>>>
>>>>>>>    Example:
>>>>>>>    {
>>>>>>>       "resource": "product",
>>>>>>>       "resource_scopes": [
>>>>>>>           { "name": "sell", "claims": { "tenant": ["Organization-1"]
>>>>>>> } },
>>>>>>>           { "name": "update", "claims": { "tenant":
>>>>>>> ["Organization-2"] } },
>>>>>>>       ]
>>>>>>>
>>>>>>> Thanks in advance,
>>>>>>> Álvaro.
>>>>>>> _______________________________________________
>>>>>>> keycloak-user mailing list
>>>>>>> keycloak-user at lists.jboss.org
>>>>>>> https://lists.jboss.org/mailman/listinfo/keycloak-user
>>>>>>
>>>>>>


More information about the keycloak-user mailing list