Skip to content

Instantly share code, notes, and snippets.

@k-jingyang
Last active May 26, 2021 13:02
Show Gist options
  • Save k-jingyang/af2c3f2f3c2cfb1fea9e1c7d94142083 to your computer and use it in GitHub Desktop.
Save k-jingyang/af2c3f2f3c2cfb1fea9e1c7d94142083 to your computer and use it in GitHub Desktop.
Scalability of Keycloak

PC Specs

My poor laptop with:

i5-82500U CPU @ 1.6Ghz

8 GB Ram

Setup

Docker compose with Keycloak + PostgreSQL

Background

Experiment was to test whether Keycloak can scale to our requirements of fine-grained access control.

Terminology

To at least be familiar with terminology of resource, resource's scope, permission, policy in the context of Authorization

Keycloak docs explain it best

Modelling

We'll model a Pokemon as resource, and the different read/write access of its different sections of data as scope

E.g. Pokemon-1 as a resource, and location-read as a scope under this resource.

Total number of resource = number of Pokemon

Total number of scope = number of Pokemon x 2 (access, i.e. read & write) x number of data sections.

We have modelled the "permissions" (not to be confused with Keycloak's permission) of our data, but how do we control who have these "permissions"? This is done through the use of permission and policy.

Best introduced with an example.

To assign any user with the realm role VIRTUAL_TEAM_1 to have access to Pokemon-1's location-read.

We have to

  1. Create a role-based policy to allow any user with the roleVIRTUAL_TEAM_1.
  2. Create a scope-based permissionthat specifies the specific resource (i.e. Pokemon-1), and its scope (i.e. location-read), and select the policy we created in the previous step.

If we want to allow VIRTUAL_TEAM_2 to also have the same access, all we need to do is to associate the VRITUAL_TEAM_2 policy with the scope-based permission created. Note that the decision_strategy for this permission have to be set as affirmative

Because we have to create a permission for each scope x resource to specify who can access the scope of a particular resource (e.g. Pokemon-1's location-read):

The total number of permission = number of resource x scope = number of Pokemon x 2 (access, i.e. read & write) x number of data sections

and..

The total number of policy = Number of VIRTUAL_TEAM roles (or however we want to group our users, because there are other kinds of policies e.g. user-based policy).

Scale & Test Results

With the data model in mind,

I created:

  • 270,000 Pokemons (resource), and for each resource, 14 read/write data section (scope).
    • Hence, there are 3,780,000 resource,scope pair
  • 6 VIRTUAL_TEAM role-based policy
  • For each resource-scope pair, a permission that associates all 6 VIRTUAL_TEAM policy
    • Hence, there are 3,780,000 permission
    • Internally, the database have to store the permission to policy mapping, this results in a table with slightly over 18,000,000 records

API Test

Using a user alice with VIRTUAL_TEAM_1 role

  1. Get a token first
$ ALICE_TOKEN=$(curl -L -X POST 'http://localhost:8080/auth/realms/master/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=pokemon-db' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_secret=1821e008-13c5-430d-959e-06c890c687e4' \
--data-urlencode 'scope=openid' \
--data-urlencode 'username=alice' \
--data-urlencode 'password=P@ssw0rd123' | jq -r .access_token)
  1. Query for the scopes of a particular resource that she has access to
$ curl http://localhost:8080/auth/realms/master/protocol/openid-connect/token \
  -H "Authorization: Bearer "$ALICE_TOKEN \
  --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
  --data "audience=pokemon-db" --data "permission=Pokemon-1" --data "response_mode=permissions"

[{"scopes":["location-read"],"rsid":"cc78b064-8096-4bd7-8f0f-76600ce2f8cb","rsname":"Pokemon-1"}]

took 42s on the first query, and <50ms on subsequent queries. This is likely due to caching. If we try to remove some form of caching by modifying a permission and firing a subsequent query that relies on that permission, the query takes around 2s.

UI Test

Loading of specific fields under the Authorization tab under Client hangs (or maybe it just takes a looooong time).

Examples include:

  • List of resource that have a particular scope
    • Most probably due to the fact that many resource have the same scope
  • List of permission that are dependent on a particularpolicy
    • Most probably due to the fact that many permission have the same policy
  • Evaluate tab, where we can test and evaluate policies given a user, roles
    • Most probably because the UI tries to fetch all permission on all resource (even though we may only specify a particular resource)

To create 270,000 resources

#!/bin/bash

for (( c=0; c<=270000; c++))
do 
    RANDOM_NO=$(shuf -i 1000000-2000000 -n 1)
    if ! ((c % 1000)); then
        echo $c

        TOKEN=$(curl -s -X POST -H "Content-Type: application/x-www-form-urlencoded" \
        -d 'grant_type=client_credentials&client_id=pokemon-db&client_secret=1821e008-13c5-430d-959e-06c890c687e4' \
        "http://localhost:8080/auth/realms/master/protocol/openid-connect/token" | jq -r .access_token) 
        
        curl -s -X POST http://localhost:8080/auth/realms/master/authz/protection/resource_set -H 'Authorization: Bearer '$TOKEN \
            -H 'Content-Type: application/json' \
            -d "{
                \"name\":\"Pokemon-$RANDOM_NO\",
                \"resource_scopes\":[
                    \"view-section1\",
                    \"edit-section1\",
                    \"view-section2\",
                    \"edit-section2\",
                    \"view-section3\",
                    \"edit-section3\",
                    \"view-section4\",
                    \"edit-section4\",
                    \"view-section5\",
                    \"edit-section5\",
                    \"view-section6\",
                    \"edit-section6\",
                    \"view-section7\",
                    \"edit-section7\"
                ]
            }" 
    else
        curl -s -X POST http://localhost:8080/auth/realms/master/authz/protection/resource_set -H 'Authorization: Bearer '$TOKEN \
        -H 'Content-Type: application/json' \
        -d "{
            \"name\":\"Pokemon-$RANDOM_NO\",
            \"resource_scopes\":[
                \"view-section1\",
                \"edit-section1\",
                \"view-section2\",
                \"edit-section2\",
                \"view-section3\",
                \"edit-section3\",
                \"view-section4\",
                \"edit-section4\",
                \"view-section5\",
                \"edit-section5\",
                \"view-section6\",
                \"edit-section6\",
                \"view-section7\",
                \"edit-section7\"
            ]
        }" >> /dev/null
    fi
done

Manually create 6 VIRTUAL_TEAM roles, and 6 role-based policies on the UI

Title.

Remember to drop the constraints in the tables when bulk populating them and add them in later

This is to speed up the inserts.

Populating of permissions via SQL

In the database, they store permission like a policy, so it can be confusing.

insert into resource_server_policy(id, name, description, type, decision_strategy, logic, resource_server_id, owner)
select permutate.unq_id as id, permutate.unq_id as name, permutate.unq_id as description, 'scope' as type , 1 as decision_strategy, 0 as logic, 'bb39ee5f-61e7-4246-8459-bf3966dc27b9' as resource_server_id, null as owner
from 
(select 
overlay(res.id placing substring(scope.scope_id from 10 for 2) from 10 for 2) as unq_id, 
 res.id as resource_id, scope.scope_id 
from resource_server_resource res 
cross join (select * from 
(VALUES ('d14ec06e-f059-46d1-b489-07450191c182'), 
('f3aa7f5d-69c3-44c5-b816-dd67464c81ab'), 
('7fc8620a-8099-47c9-b635-c985576a674a'),
('a265d48e-8455-4fd4-b8a1-25fb8c7f1d9b'),
('b6168ff1-f743-4a0f-a19e-81b753d1d2ae'),
('d555eae7-96af-4731-bdb1-e16b3b9500e2'),
('d84c6b42-854c-45d8-a1e6-43c3fbbaaf91'),
('95b47c96-62c0-4750-b66e-fb267af416db'),
('e7a7c368-6441-414b-b7c6-4cc76e357f0f'),
('ba0ff3cc-45bd-46f4-a1d4-f683aee341c7'),
('128e3220-a53f-40a4-9e07-090440db0e1b'),
('9e3b39d7-e9a7-4eb9-98de-ffbaab3a8971'),
('b4b47351-399d-4ea9-ad11-1bb34306d58e'),
('d69b4bdb-762c-468d-bea1-563d70a74c82')) as scope(scope_id)) scope  
where name like 'Pokemon-%') permutate

where the hardcoded ids can be found in resource_server_scope. They are the ids of scope that are created. I created the same 14 scope for each resource.

To create a scope-based permission,

insert into scope_policy(scope_id, policy_id)
select permutate.scope_id, permutate.unq_id as policy_id
from 
(select 
overlay(res.id placing substring(scope.scope_id from 10 for 2) from 10 for 2) as unq_id, 
 res.id as resource_id, scope.scope_id 
from resource_server_resource res 
cross join (select * from (VALUES ('d14ec06e-f059-46d1-b489-07450191c182'), 
('f3aa7f5d-69c3-44c5-b816-dd67464c81ab'), 
('7fc8620a-8099-47c9-b635-c985576a674a'),
('a265d48e-8455-4fd4-b8a1-25fb8c7f1d9b'),
('b6168ff1-f743-4a0f-a19e-81b753d1d2ae'),
('d555eae7-96af-4731-bdb1-e16b3b9500e2'),
('d84c6b42-854c-45d8-a1e6-43c3fbbaaf91'),
('95b47c96-62c0-4750-b66e-fb267af416db'),
('e7a7c368-6441-414b-b7c6-4cc76e357f0f'),
('ba0ff3cc-45bd-46f4-a1d4-f683aee341c7'),
('128e3220-a53f-40a4-9e07-090440db0e1b'),
('9e3b39d7-e9a7-4eb9-98de-ffbaab3a8971'),
('b4b47351-399d-4ea9-ad11-1bb34306d58e'),
('d69b4bdb-762c-468d-bea1-563d70a74c82')) as scope(scope_id)) scope 
 where name like 'Pokemon-%') permutate

Even though only we want a scope-based permission, the scope-based permission for our use case is tied to a specific resource. Hence, in the database backend, the same permission is also a resource-based permission

insert into resource_policy(resource_id, policy_id)
select permutate.resource_id, permutate.unq_id as policy_id
from 
(select 
overlay(res.id placing substring(scope.scope_id from 10 for 2) from 10 for 2) as unq_id, 
res.id as resource_id, scope.scope_id 
from resource_server_resource res 
cross join (select * from (VALUES ('d14ec06e-f059-46d1-b489-07450191c182'), 
('f3aa7f5d-69c3-44c5-b816-dd67464c81ab'), 
('7fc8620a-8099-47c9-b635-c985576a674a'),
('a265d48e-8455-4fd4-b8a1-25fb8c7f1d9b'),
('b6168ff1-f743-4a0f-a19e-81b753d1d2ae'),
('d555eae7-96af-4731-bdb1-e16b3b9500e2'),
('d84c6b42-854c-45d8-a1e6-43c3fbbaaf91'),
('95b47c96-62c0-4750-b66e-fb267af416db'),
('e7a7c368-6441-414b-b7c6-4cc76e357f0f'),
('ba0ff3cc-45bd-46f4-a1d4-f683aee341c7'),
('128e3220-a53f-40a4-9e07-090440db0e1b'),
('9e3b39d7-e9a7-4eb9-98de-ffbaab3a8971'),
('b4b47351-399d-4ea9-ad11-1bb34306d58e'),
('d69b4bdb-762c-468d-bea1-563d70a74c82')) as scope(scope_id)) scope 
where name like 'Pokemon-%') permutate

Associate role-based policy with the permissions via SQL

insert into associated_policy(policy_id, associated_policy_id)
select rsp.id, role_policy.role_policy_id
from resource_server_policy rsp
cross join (select * from (
values
('d088899f-17df-4a5c-bbf8-adf95141783c'),
('73498845-6386-4ab3-9855-b205ba817f49'),
('492f7a02-275d-4a23-9506-aa2bb9f149af'),
('aa56acb6-73fb-4285-acc6-cb3c0e15d4d9'),
('4d308edb-d8a9-4186-afcf-5e41ad04336b'),
('51564ba0-bfbd-4603-b17b-a83fd7c89519')) as role_policy(role_policy_id)) role_policy

where the hardcoded ids are the 6 VIRTUAL_TEAM role-based policies. These can be found in resource_server_policy with the type role and the names.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment