Browse Source

feat: add client scope client-specific role mappings (#605)

Co-authored-by: Justin Ryan <j.ryan@mwam.com>
master v4.7.0
Justin Ryan 2 weeks ago
committed by GitHub
parent
commit
52b716562f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 223
      src/keycloak/keycloak_admin.py
  2. 5
      src/keycloak/urls_patterns.py
  3. 176
      tests/test_keycloak_admin.py

223
src/keycloak/keycloak_admin.py

@ -2602,7 +2602,9 @@ class KeycloakAdmin:
return raise_error_from_response(data_raw, KeycloakGetError) return raise_error_from_response(data_raw, KeycloakGetError)
def assign_client_roles_to_client_scope(self, client_id, client_roles_owner_id, roles): def assign_client_roles_to_client_scope(self, client_id, client_roles_owner_id, roles):
"""Assign client roles to a client's scope.
"""Assign client roles to a client's dedicated scope.
To assign roles to a client scope, use add_client_specific_roles_to_client_scope.
:param client_id: id of client (not client-id) who is assigned the roles :param client_id: id of client (not client-id) who is assigned the roles
:type client_id: str :type client_id: str
@ -2626,7 +2628,9 @@ class KeycloakAdmin:
return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204])
def delete_client_roles_of_client_scope(self, client_id, client_roles_owner_id, roles): def delete_client_roles_of_client_scope(self, client_id, client_roles_owner_id, roles):
"""Delete client roles of a client's scope.
"""Delete client roles of a client's dedicated scope.
To delete roles from a client scope, use remove_client_specific_roles_of_client_scope.
:param client_id: id of client (not client-id) who is assigned the roles :param client_id: id of client (not client-id) who is assigned the roles
:type client_id: str :type client_id: str
@ -2650,7 +2654,9 @@ class KeycloakAdmin:
return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204])
def get_client_roles_of_client_scope(self, client_id, client_roles_owner_id): def get_client_roles_of_client_scope(self, client_id, client_roles_owner_id):
"""Get all client roles for a client's scope.
"""Get all client roles for a client's dedicated scope.
To get roles for a client scope, use get_client_specific_roles_of_client_scope.
:param client_id: id of client (not client-id) :param client_id: id of client (not client-id)
:type client_id: str :type client_id: str
@ -3574,6 +3580,104 @@ class KeycloakAdmin:
) )
return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204])
def add_client_specific_roles_to_client_scope(
self, client_scope_id, client_roles_owner_id, roles
):
"""Assign client roles to a client scope.
To assign roles to a client's dedicated scope, use assign_client_roles_to_client_scope.
:param client_scope_id: client scope id
:type client_scope_id: str
:param client_roles_owner_id: id of client (not client-id) who has the roles
:type client_roles_owner_id: str
:param roles: roles list or role (use RoleRepresentation, must include id and name)
:type roles: list
:return: Keycloak server response
:rtype: dict
"""
payload = roles if isinstance(roles, list) else [roles]
params_path = {
"realm-name": self.connection.realm_name,
"scope-id": client_scope_id,
"client-id": client_roles_owner_id,
}
data_raw = self.connection.raw_post(
urls_patterns.URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS_CLIENT.format(**params_path),
data=json.dumps(payload),
)
return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204])
def remove_client_specific_roles_of_client_scope(
self, client_scope_id, client_roles_owner_id, roles
):
"""Delete client roles of a client scope.
To delete roles from a client's dedicated scope, use delete_client_roles_of_client_scope.
:param client_scope_id: client scope id
:type client_scope_id: str
:param client_roles_owner_id: id of client (not client-id) who has the roles
:type client_roles_owner_id: str
:param roles: roles list or role (use RoleRepresentation, must include id and name)
:type roles: list
:return: Keycloak server response
:rtype: dict
"""
payload = roles if isinstance(roles, list) else [roles]
params_path = {
"realm-name": self.connection.realm_name,
"scope-id": client_scope_id,
"client-id": client_roles_owner_id,
}
data_raw = self.connection.raw_delete(
urls_patterns.URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS_CLIENT.format(**params_path),
data=json.dumps(payload),
)
return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204])
def get_client_specific_roles_of_client_scope(self, client_scope_id, client_roles_owner_id):
"""Get client roles for a client scope, for a specific client.
To get roles for a client's dedicated scope, use get_client_roles_of_client_scope.
:param client_scope_id: client scope id
:type client_scope_id: str
:param client_roles_owner_id: id of client (not client-id) who has the roles
:type client_roles_owner_id: str
:return: Keycloak server response (array RoleRepresentation)
:rtype: dict
"""
params_path = {
"realm-name": self.connection.realm_name,
"scope-id": client_scope_id,
"client-id": client_roles_owner_id,
}
data_raw = self.connection.raw_get(
urls_patterns.URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS_CLIENT.format(**params_path)
)
return raise_error_from_response(data_raw, KeycloakGetError)
def get_all_roles_of_client_scope(self, client_scope_id):
"""Get all client roles for a client scope.
To get roles for a client's dedicated scope,
use get_client_roles_of_client_scope.
:param client_scope_id: client scope id
:type client_scope_id: str
:return: Keycloak server response (array RoleRepresentation)
:rtype: dict
"""
params_path = {
"realm-name": self.connection.realm_name,
"scope-id": client_scope_id,
}
data_raw = self.connection.raw_get(
urls_patterns.URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS.format(**params_path)
)
return raise_error_from_response(data_raw, KeycloakGetError)
def get_mappers_from_client(self, client_id): def get_mappers_from_client(self, client_id):
"""List of all client mappers. """List of all client mappers.
@ -6816,7 +6920,9 @@ class KeycloakAdmin:
return raise_error_from_response(data_raw, KeycloakGetError) return raise_error_from_response(data_raw, KeycloakGetError)
async def a_assign_client_roles_to_client_scope(self, client_id, client_roles_owner_id, roles): async def a_assign_client_roles_to_client_scope(self, client_id, client_roles_owner_id, roles):
"""Assign client roles to a client's scope asynchronously.
"""Assign client roles to a client's dedicated scope asynchronously.
To assign roles to a client scope, use a_add_client_specific_roles_to_client_scope.
:param client_id: id of client (not client-id) who is assigned the roles :param client_id: id of client (not client-id) who is assigned the roles
:type client_id: str :type client_id: str
@ -6840,7 +6946,9 @@ class KeycloakAdmin:
return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204])
async def a_delete_client_roles_of_client_scope(self, client_id, client_roles_owner_id, roles): async def a_delete_client_roles_of_client_scope(self, client_id, client_roles_owner_id, roles):
"""Delete client roles of a client's scope asynchronously.
"""Delete client roles of a client's dedicated scope asynchronously.
To remove roles from a client scope, use a_remove_client_specific_roles_of_client_scope.
:param client_id: id of client (not client-id) who is assigned the roles :param client_id: id of client (not client-id) who is assigned the roles
:type client_id: str :type client_id: str
@ -6866,6 +6974,8 @@ class KeycloakAdmin:
async def a_get_client_roles_of_client_scope(self, client_id, client_roles_owner_id): async def a_get_client_roles_of_client_scope(self, client_id, client_roles_owner_id):
"""Get all client roles for a client's scope asynchronously. """Get all client roles for a client's scope asynchronously.
To get roles from a client scope, use a_get_client_roles_of_client_scope.
:param client_id: id of client (not client-id) :param client_id: id of client (not client-id)
:type client_id: str :type client_id: str
:param client_roles_owner_id: id of client (not client-id) who has the roles :param client_roles_owner_id: id of client (not client-id) who has the roles
@ -7794,6 +7904,109 @@ class KeycloakAdmin:
) )
return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204])
async def a_add_client_specific_roles_to_client_scope(
self, client_scope_id, client_roles_owner_id, roles
):
"""Assign client roles to a client scope asynchronously.
To assign roles to a client's dedicated scope, use
a_assign_client_roles_to_client_scope.
:param client_scope_id: client scope id
:type client_scope_id: str
:param client_roles_owner_id: id of client (not client-id) who has the roles
:type client_roles_owner_id: str
:param roles: roles list or role (use RoleRepresentation, must include id and name)
:type roles: list
:return: Keycloak server response
:rtype: dict
"""
payload = roles if isinstance(roles, list) else [roles]
params_path = {
"realm-name": self.connection.realm_name,
"scope-id": client_scope_id,
"client-id": client_roles_owner_id,
}
data_raw = await self.connection.a_raw_post(
urls_patterns.URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS_CLIENT.format(**params_path),
data=json.dumps(payload),
)
return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204])
async def a_remove_client_specific_roles_of_client_scope(
self, client_scope_id, client_roles_owner_id, roles
):
"""Delete client roles of a client scope asynchronously.
To delete roles from a client's dedicated scope,
use a_delete_client_roles_of_client_scope.
:param client_scope_id: client scope id
:type client_scope_id: str
:param client_roles_owner_id: id of client (not client-id) who has the roles
:type client_roles_owner_id: str
:param roles: roles list or role (use RoleRepresentation, must include id and name)
:type roles: list
:return: Keycloak server response
:rtype: dict
"""
payload = roles if isinstance(roles, list) else [roles]
params_path = {
"realm-name": self.connection.realm_name,
"scope-id": client_scope_id,
"client-id": client_roles_owner_id,
}
data_raw = await self.connection.a_raw_delete(
urls_patterns.URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS_CLIENT.format(**params_path),
data=json.dumps(payload),
)
return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204])
async def a_get_client_specific_roles_of_client_scope(
self, client_scope_id, client_roles_owner_id
):
"""Get all client roles for a client scope asynchronously.
To get roles for a client's dedicated scope,
use a_get_client_roles_of_client_scope.
:param client_scope_id: client scope id
:type client_scope_id: str
:param client_roles_owner_id: id of client (not client-id) who has the roles
:type client_roles_owner_id: str
:return: Keycloak server response (array RoleRepresentation)
:rtype: dict
"""
params_path = {
"realm-name": self.connection.realm_name,
"scope-id": client_scope_id,
"client-id": client_roles_owner_id,
}
data_raw = await self.connection.a_raw_get(
urls_patterns.URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS_CLIENT.format(**params_path)
)
return raise_error_from_response(data_raw, KeycloakGetError)
async def a_get_all_roles_of_client_scope(self, client_scope_id):
"""Get all client roles for a client scope.
To get roles for a client's dedicated scope,
use a_get_client_roles_of_client_scope.
:param client_scope_id: client scope id
:type client_scope_id: str
:return: Keycloak server response (array RoleRepresentation)
:rtype: dict
"""
params_path = {
"realm-name": self.connection.realm_name,
"scope-id": client_scope_id,
}
data_raw = self.connection.raw_get(
urls_patterns.URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS.format(**params_path)
)
return raise_error_from_response(data_raw, KeycloakGetError)
async def a_get_mappers_from_client(self, client_id): async def a_get_mappers_from_client(self, client_id):
"""List of all client mappers asynchronously. """List of all client mappers asynchronously.

5
src/keycloak/urls_patterns.py

@ -148,6 +148,11 @@ URL_ADMIN_CLIENT_SCOPES = "admin/realms/{realm-name}/client-scopes"
URL_ADMIN_CLIENT_SCOPE = URL_ADMIN_CLIENT_SCOPES + "/{scope-id}" URL_ADMIN_CLIENT_SCOPE = URL_ADMIN_CLIENT_SCOPES + "/{scope-id}"
URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER = URL_ADMIN_CLIENT_SCOPE + "/protocol-mappers/models" URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER = URL_ADMIN_CLIENT_SCOPE + "/protocol-mappers/models"
URL_ADMIN_CLIENT_SCOPES_MAPPERS = URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER + "/{protocol-mapper-id}" URL_ADMIN_CLIENT_SCOPES_MAPPERS = URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER + "/{protocol-mapper-id}"
URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS = URL_ADMIN_CLIENT_SCOPE + "/scope-mappings"
URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS_REALM = URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS + "/realm"
URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS_CLIENT = (
URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS + "/clients/{client-id}"
)
URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles"
URL_ADMIN_REALM_ROLES_SEARCH = URL_ADMIN_REALM_ROLES + "?search={search-text}" URL_ADMIN_REALM_ROLES_SEARCH = URL_ADMIN_REALM_ROLES + "?search={search-text}"

176
tests/test_keycloak_admin.py

@ -1683,6 +1683,93 @@ def test_client_scope_client_roles(admin: KeycloakAdmin, realm: str, client: str
assert len(roles) == 0 assert len(roles) == 0
def test_client_scope_mapping_client_roles(admin: KeycloakAdmin, realm: str, client: str):
"""Test client scope assignment of client roles.
:param admin: Keycloak admin
:type admin: KeycloakAdmin
:param realm: Keycloak realm
:type realm: str
:param client: Keycloak client owning roles
:type client: str
"""
CLIENT_ROLE_NAME = "some-client-role"
admin.change_current_realm(realm)
client_name = admin.get_client(client)["name"]
client_scope = {
"name": "test_client_scope",
"description": "Test Client Scope",
"protocol": "openid-connect",
"attributes": {},
}
client_scope_id = admin.create_client_scope(client_scope, skip_exists=False)
# Test get client roles
client_specific_roles = admin.get_client_specific_roles_of_client_scope(
client_scope_id, client
)
assert len(client_specific_roles) == 0, client_specific_roles
all_roles = admin.get_all_roles_of_client_scope(client_scope_id)
assert len(all_roles) == 0, all_roles
# create client role for test
client_role_name = admin.create_client_role(
client_role_id=client, payload={"name": CLIENT_ROLE_NAME}, skip_exists=True
)
assert client_role_name, client_role_name
# Test client role assignment to other client
with pytest.raises(KeycloakPostError) as err:
admin.add_client_specific_roles_to_client_scope(
client_scope_id=client_scope_id, client_roles_owner_id=client, roles=["bad"]
)
assert err.match(UNKOWN_ERROR_REGEX), err
res = admin.add_client_specific_roles_to_client_scope(
client_scope_id=client_scope_id,
client_roles_owner_id=client,
roles=[admin.get_client_role(client_id=client, role_name=CLIENT_ROLE_NAME)],
)
assert res == dict(), res
# Test when getting roles for the specific owner client
client_specific_roles = admin.get_client_specific_roles_of_client_scope(
client_scope_id=client_scope_id, client_roles_owner_id=client
)
assert len(client_specific_roles) == 1
client_role_names = [x["name"] for x in client_specific_roles]
assert CLIENT_ROLE_NAME in client_role_names, client_role_names
# Test when getting all roles for the client scope
all_roles = admin.get_all_roles_of_client_scope(client_scope_id=client_scope_id)
assert "clientMappings" in all_roles, all_roles
all_roles_clients = all_roles["clientMappings"]
assert client_name in all_roles_clients, all_roles_clients
mappings = all_roles_clients[client_name]["mappings"]
client_role_names = [x["name"] for x in mappings]
assert CLIENT_ROLE_NAME in client_role_names, client_role_names
# Test remove realm role of client
with pytest.raises(KeycloakDeleteError) as err:
admin.remove_client_specific_roles_of_client_scope(
client_scope_id=client_scope_id, client_roles_owner_id=client, roles=["bad"]
)
assert err.match(UNKOWN_ERROR_REGEX), err
res = admin.remove_client_specific_roles_of_client_scope(
client_scope_id=client_scope_id,
client_roles_owner_id=client,
roles=[admin.get_client_role(client_id=client, role_name=CLIENT_ROLE_NAME)],
)
assert res == dict(), res
all_roles = admin.get_all_roles_of_client_scope(client_scope_id=client_scope_id)
assert len(all_roles) == 0
def test_client_default_client_scopes(admin: KeycloakAdmin, realm: str, client: str): def test_client_default_client_scopes(admin: KeycloakAdmin, realm: str, client: str):
"""Test client assignment of default client scopes. """Test client assignment of default client scopes.
@ -4729,6 +4816,95 @@ async def test_a_client_scope_client_roles(admin: KeycloakAdmin, realm: str, cli
assert len(roles) == 0 assert len(roles) == 0
@pytest.mark.asyncio
async def test_a_client_scope_mapping_client_roles(admin: KeycloakAdmin, realm: str, client: str):
"""Test client scope assignment of client roles.
:param admin: Keycloak admin
:type admin: KeycloakAdmin
:param realm: Keycloak realm
:type realm: str
:param client: Keycloak client owning roles
:type client: str
"""
CLIENT_ROLE_NAME = "some-client-role"
await admin.a_change_current_realm(realm)
client_obj = await admin.a_get_client(client)
client_name = client_obj["name"]
client_scope = {
"name": "test_client_scope",
"description": "Test Client Scope",
"protocol": "openid-connect",
"attributes": {},
}
client_scope_id = await admin.a_create_client_scope(client_scope, skip_exists=False)
# Test get client roles
client_specific_roles = await admin.a_get_client_specific_roles_of_client_scope(
client_scope_id, client
)
assert len(client_specific_roles) == 0, client_specific_roles
all_roles = await admin.a_get_all_roles_of_client_scope(client_scope_id)
assert len(all_roles) == 0, all_roles
# create client role for test
client_role_name = await admin.a_create_client_role(
client_role_id=client, payload={"name": CLIENT_ROLE_NAME}, skip_exists=True
)
assert client_role_name, client_role_name
# Test client role assignment to other client
with pytest.raises(KeycloakPostError) as err:
await admin.a_add_client_specific_roles_to_client_scope(
client_scope_id=client_scope_id, client_roles_owner_id=client, roles=["bad"]
)
assert err.match(UNKOWN_ERROR_REGEX), err
res = await admin.a_add_client_specific_roles_to_client_scope(
client_scope_id=client_scope_id,
client_roles_owner_id=client,
roles=[await admin.a_get_client_role(client_id=client, role_name=CLIENT_ROLE_NAME)],
)
assert res == dict(), res
# Test when getting roles for the specific owner client
client_specific_roles = await admin.a_get_client_specific_roles_of_client_scope(
client_scope_id=client_scope_id, client_roles_owner_id=client
)
assert len(client_specific_roles) == 1
client_role_names = [x["name"] for x in client_specific_roles]
assert CLIENT_ROLE_NAME in client_role_names, client_role_names
# Test when getting all roles for the client scope
all_roles = await admin.a_get_all_roles_of_client_scope(client_scope_id=client_scope_id)
assert "clientMappings" in all_roles, all_roles
all_roles_clients = all_roles["clientMappings"]
assert client_name in all_roles_clients, all_roles_clients
mappings = all_roles_clients[client_name]["mappings"]
client_role_names = [x["name"] for x in mappings]
assert CLIENT_ROLE_NAME in client_role_names, client_role_names
# Test remove realm role of client
with pytest.raises(KeycloakDeleteError) as err:
await admin.a_remove_client_specific_roles_of_client_scope(
client_scope_id=client_scope_id, client_roles_owner_id=client, roles=["bad"]
)
assert err.match(UNKOWN_ERROR_REGEX), err
res = await admin.a_remove_client_specific_roles_of_client_scope(
client_scope_id=client_scope_id,
client_roles_owner_id=client,
roles=[await admin.a_get_client_role(client_id=client, role_name=CLIENT_ROLE_NAME)],
)
assert res == dict(), res
all_roles = await admin.a_get_all_roles_of_client_scope(client_scope_id=client_scope_id)
assert len(all_roles) == 0
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_a_client_default_client_scopes(admin: KeycloakAdmin, realm: str, client: str): async def test_a_client_default_client_scopes(admin: KeycloakAdmin, realm: str, client: str):
"""Test client assignment of default client scopes. """Test client assignment of default client scopes.

Loading…
Cancel
Save