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(a)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@Organization-1
* Product-Manager@Organization-2
* Is-Seller@Organization-1
* Is-Seller@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(a)redhat.com>)
escribió:
> On Thu, Jul 25, 2019 at 10:13 AM Álvaro Gómez <
> alvaro.gomez.gimenez(a)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(a)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(a)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(a)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(a)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(a)lists.jboss.org
>>>>>>
https://lists.jboss.org/mailman/listinfo/keycloak-user
>>>>>
>>>>>