[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