Browse Source

Updating to use async for keycloak calls

pull/585/head
gregmccoy 2 years ago
parent
commit
8907ae3410
  1. 2
      CHANGELOG.md
  2. 1740
      poetry.lock
  3. 4
      pyproject.toml
  4. 40
      src/keycloak/connection.py
  5. 4
      src/keycloak/exceptions.py
  6. 838
      src/keycloak/keycloak_admin.py
  7. 67
      src/keycloak/keycloak_openid.py
  8. 105
      tests/conftest.py
  9. 1106
      tests/test_keycloak_admin.py
  10. 186
      tests/test_keycloak_openid.py
  11. 34
      tox.ini

2
CHANGELOG.md

@ -1,3 +1,5 @@
## Unreleased
## v2.9.0 (2023-01-11)
### Feat

1740
poetry.lock
File diff suppressed because it is too large
View File

4
pyproject.toml

@ -30,7 +30,7 @@ Documentation = "https://python-keycloak.readthedocs.io/en/latest/"
[tool.poetry.dependencies]
python = "^3.7"
requests = "^2.20.0"
httpx = "^0.23.0"
python-jose = "^3.3.0"
urllib3 = "^1.26.0"
mock = {version = "^4.0.3", optional = true}
@ -42,7 +42,6 @@ sphinx-rtd-theme = {version = "^1.0.0", optional = true}
readthedocs-sphinx-ext = {version = "^2.1.9", optional = true}
m2r2 = {version = "^0.3.2", optional = true}
sphinx-autoapi = {version = "^2.0.0", optional = true}
requests-toolbelt = "^0.9.1"
[tool.poetry.extras]
docs = [
@ -61,6 +60,7 @@ docs = [
tox = "^3.25.0"
pytest = "^7.1.2"
pytest-cov = "^3.0.0"
pytest-asyncio = "0.20.3"
wheel = "^0.37.1"
pre-commit = "^2.19.0"
isort = "^5.10.1"

40
src/keycloak/connection.py

@ -28,8 +28,7 @@ try:
except ImportError: # pragma: no cover
from urlparse import urljoin
import requests
from requests.adapters import HTTPAdapter
import httpx
from .exceptions import KeycloakConnectionError
@ -67,26 +66,19 @@ class ConnectionManager(object):
self.headers = headers
self.timeout = timeout
self.verify = verify
self._s = requests.Session()
self._s = httpx.AsyncClient(verify=verify)
self._s.auth = lambda x: x # don't let requests add auth headers
# retry once to reset connection with Keycloak after tomcat's ConnectionTimeout
# see https://github.com/marcospereirampj/python-keycloak/issues/36
for protocol in ("https://", "http://"):
adapter = HTTPAdapter(max_retries=1)
# adds POST to retry whitelist
allowed_methods = set(adapter.max_retries.allowed_methods)
allowed_methods.add("POST")
adapter.max_retries.allowed_methods = frozenset(allowed_methods)
self._s.mount(protocol, adapter)
self._s.transport = httpx.AsyncHTTPTransport(retries=1)
if proxies:
self._s.proxies.update(proxies)
def __del__(self):
async def close(self):
"""Del method."""
self._s.close()
await self._s.aclose()
@property
def base_url(self):
@ -182,7 +174,7 @@ class ConnectionManager(object):
"""
self.headers.pop(key, None)
def raw_get(self, path, **kwargs):
async def raw_get(self, path, **kwargs):
"""Submit get request to the path.
:param path: Path for request.
@ -194,17 +186,17 @@ class ConnectionManager(object):
:raises KeycloakConnectionError: HttpError Can't connect to server.
"""
try:
return self._s.get(
return await self._s.request(
"GET",
urljoin(self.base_url, path),
params=kwargs,
headers=self.headers,
timeout=self.timeout,
verify=self.verify,
)
except Exception as e:
raise KeycloakConnectionError("Can't connect to server (%s)" % e)
def raw_post(self, path, data, **kwargs):
async def raw_post(self, path, data, **kwargs):
"""Submit post request to the path.
:param path: Path for request.
@ -218,18 +210,17 @@ class ConnectionManager(object):
:raises KeycloakConnectionError: HttpError Can't connect to server.
"""
try:
return self._s.post(
return await self._s.post(
urljoin(self.base_url, path),
params=kwargs,
data=data,
headers=self.headers,
timeout=self.timeout,
verify=self.verify,
)
except Exception as e:
raise KeycloakConnectionError("Can't connect to server (%s)" % e)
def raw_put(self, path, data, **kwargs):
async def raw_put(self, path, data, **kwargs):
"""Submit put request to the path.
:param path: Path for request.
@ -243,18 +234,17 @@ class ConnectionManager(object):
:raises KeycloakConnectionError: HttpError Can't connect to server.
"""
try:
return self._s.put(
return await self._s.put(
urljoin(self.base_url, path),
params=kwargs,
data=data,
headers=self.headers,
timeout=self.timeout,
verify=self.verify,
)
except Exception as e:
raise KeycloakConnectionError("Can't connect to server (%s)" % e)
def raw_delete(self, path, data=None, **kwargs):
async def raw_delete(self, path, data=None, **kwargs):
"""Submit delete request to the path.
:param path: Path for request.
@ -268,13 +258,13 @@ class ConnectionManager(object):
:raises KeycloakConnectionError: HttpError Can't connect to server.
"""
try:
return self._s.delete(
return await self._s.request(
"DELETE",
urljoin(self.base_url, path),
params=kwargs,
data=data or dict(),
headers=self.headers,
timeout=self.timeout,
verify=self.verify,
)
except Exception as e:
raise KeycloakConnectionError("Can't connect to server (%s)" % e)

4
src/keycloak/exceptions.py

@ -23,8 +23,6 @@
"""Keycloak custom exceptions module."""
import requests
class KeycloakError(Exception):
"""Base class for custom Keycloak errors.
@ -167,7 +165,7 @@ def raise_error_from_response(response, error, expected_codes=None, skip_exists=
expected_codes = [200, 201, 204]
if response.status_code in expected_codes:
if response.status_code == requests.codes.no_content:
if response.status_code == 204:
return {}
try:

838
src/keycloak/keycloak_admin.py
File diff suppressed because it is too large
View File

67
src/keycloak/keycloak_openid.py

@ -198,7 +198,7 @@ class KeycloakOpenID:
"""
return self.client_id + "/" + role
def _token_info(self, token, method_token_info, **kwargs):
async def _token_info(self, token, method_token_info, **kwargs):
"""Getter for the token data.
:param token: Token
@ -211,13 +211,13 @@ class KeycloakOpenID:
:rtype: dict
"""
if method_token_info == "introspect":
token_info = self.introspect(token)
token_info = await self.introspect(token)
else:
token_info = self.decode_token(token, **kwargs)
return token_info
def well_known(self):
async def well_known(self):
"""Get the well_known object.
The most important endpoint to understand is the well-known configuration
@ -228,10 +228,10 @@ class KeycloakOpenID:
:rtype: dict
"""
params_path = {"realm-name": self.realm_name}
data_raw = self.connection.raw_get(URL_WELL_KNOWN.format(**params_path))
data_raw = await self.connection.raw_get(URL_WELL_KNOWN.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)
def auth_url(self, redirect_uri, scope="email", state=""):
async def auth_url(self, redirect_uri, scope="email", state=""):
"""Get authorization URL endpoint.
:param redirect_uri: Redirect url to receive oauth code
@ -243,8 +243,9 @@ class KeycloakOpenID:
:returns: Authorization URL Full Build
:rtype: str
"""
well_known = await self.well_known()
params_path = {
"authorization-endpoint": self.well_known()["authorization_endpoint"],
"authorization-endpoint": well_known["authorization_endpoint"],
"client-id": self.client_id,
"redirect-uri": redirect_uri,
"scope": scope,
@ -252,7 +253,7 @@ class KeycloakOpenID:
}
return URL_AUTH.format(**params_path)
def token(
async def token(
self,
username="",
password="",
@ -308,10 +309,10 @@ class KeycloakOpenID:
payload["totp"] = totp
payload = self._add_secret_key(payload)
data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload)
data_raw = await self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload)
return raise_error_from_response(data_raw, KeycloakPostError)
def refresh_token(self, refresh_token, grant_type=["refresh_token"]):
async def refresh_token(self, refresh_token, grant_type=["refresh_token"]):
"""Refresh the user token.
The token endpoint is used to obtain tokens. Tokens can either be obtained by
@ -335,10 +336,10 @@ class KeycloakOpenID:
"refresh_token": refresh_token,
}
payload = self._add_secret_key(payload)
data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload)
data_raw = await self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload)
return raise_error_from_response(data_raw, KeycloakPostError)
def exchange_token(
async def exchange_token(
self,
token: str,
client_id: str,
@ -378,10 +379,10 @@ class KeycloakOpenID:
"scope": scope,
}
payload = self._add_secret_key(payload)
data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload)
data_raw = await self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload)
return raise_error_from_response(data_raw, KeycloakPostError)
def userinfo(self, token):
async def userinfo(self, token):
"""Get the user info object.
The userinfo endpoint returns standard claims about the authenticated user,
@ -396,10 +397,10 @@ class KeycloakOpenID:
"""
self.connection.add_param_headers("Authorization", "Bearer " + token)
params_path = {"realm-name": self.realm_name}
data_raw = self.connection.raw_get(URL_USERINFO.format(**params_path))
data_raw = await self.connection.raw_get(URL_USERINFO.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)
def logout(self, refresh_token):
async def logout(self, refresh_token):
"""Log out the authenticated user.
:param refresh_token: Refresh token from Keycloak
@ -410,10 +411,10 @@ class KeycloakOpenID:
params_path = {"realm-name": self.realm_name}
payload = {"client_id": self.client_id, "refresh_token": refresh_token}
payload = self._add_secret_key(payload)
data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), data=payload)
data_raw = await self.connection.raw_post(URL_LOGOUT.format(**params_path), data=payload)
return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204])
def certs(self):
async def certs(self):
"""Get certificates.
The certificate endpoint returns the public keys enabled by the realm, encoded as a
@ -426,10 +427,10 @@ class KeycloakOpenID:
:rtype: dict
"""
params_path = {"realm-name": self.realm_name}
data_raw = self.connection.raw_get(URL_CERTS.format(**params_path))
data_raw = await self.connection.raw_get(URL_CERTS.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)
def public_key(self):
async def public_key(self):
"""Retrieve the public key.
The public key is exposed by the realm page directly.
@ -438,10 +439,10 @@ class KeycloakOpenID:
:rtype: str
"""
params_path = {"realm-name": self.realm_name}
data_raw = self.connection.raw_get(URL_REALM.format(**params_path))
data_raw = await self.connection.raw_get(URL_REALM.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)["public_key"]
def entitlement(self, token, resource_server_id):
async def entitlement(self, token, resource_server_id):
"""Get entitlements from the token.
Client applications can use a specific endpoint to obtain a special security token
@ -459,14 +460,14 @@ class KeycloakOpenID:
"""
self.connection.add_param_headers("Authorization", "Bearer " + token)
params_path = {"realm-name": self.realm_name, "resource-server-id": resource_server_id}
data_raw = self.connection.raw_get(URL_ENTITLEMENT.format(**params_path))
data_raw = await self.connection.raw_get(URL_ENTITLEMENT.format(**params_path))
if data_raw.status_code == 404:
return raise_error_from_response(data_raw, KeycloakDeprecationError)
return raise_error_from_response(data_raw, KeycloakGetError) # pragma: no cover
def introspect(self, token, rpt=None, token_type_hint=None):
async def introspect(self, token, rpt=None, token_type_hint=None):
"""Introspect the user token.
The introspection endpoint is used to retrieve the active state of a token.
@ -491,13 +492,13 @@ class KeycloakOpenID:
if token_type_hint == "requesting_party_token":
if rpt:
payload.update({"token": rpt, "token_type_hint": token_type_hint})
self.connection.add_param_headers("Authorization", "Bearer " + token)
await self.connection.add_param_headers("Authorization", "Bearer " + token)
else:
raise KeycloakRPTNotFound("Can't found RPT.")
payload = self._add_secret_key(payload)
data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), data=payload)
data_raw = await self.connection.raw_post(URL_INTROSPECT.format(**params_path), data=payload)
return raise_error_from_response(data_raw, KeycloakPostError)
def decode_token(self, token, key, algorithms=["RS256"], **kwargs):
@ -536,7 +537,7 @@ class KeycloakOpenID:
self.authorization.load_config(authorization_json)
def get_policies(self, token, method_token_info="introspect", **kwargs):
async def get_policies(self, token, method_token_info="introspect", **kwargs):
"""Get policies by user token.
:param token: User token
@ -555,7 +556,7 @@ class KeycloakOpenID:
"Keycloak settings not found. Load Authorization Keycloak settings."
)
token_info = self._token_info(token, method_token_info, **kwargs)
token_info = await self._token_info(token, method_token_info, **kwargs)
if method_token_info == "introspect" and not token_info["active"]:
raise KeycloakInvalidTokenError("Token expired or invalid.")
@ -574,7 +575,7 @@ class KeycloakOpenID:
return list(set(policies))
def get_permissions(self, token, method_token_info="introspect", **kwargs):
async def get_permissions(self, token, method_token_info="introspect", **kwargs):
"""Get permission by user token.
:param token: user token
@ -593,7 +594,7 @@ class KeycloakOpenID:
"Keycloak settings not found. Load Authorization Keycloak settings."
)
token_info = self._token_info(token, method_token_info, **kwargs)
token_info = await self._token_info(token, method_token_info, **kwargs)
if method_token_info == "introspect" and not token_info["active"]:
raise KeycloakInvalidTokenError("Token expired or invalid.")
@ -612,7 +613,7 @@ class KeycloakOpenID:
return list(set(permissions))
def uma_permissions(self, token, permissions=""):
async def uma_permissions(self, token, permissions=""):
"""Get UMA permissions by user token with requested permissions.
The token endpoint is used to retrieve UMA permissions from Keycloak. It can only be
@ -638,10 +639,10 @@ class KeycloakOpenID:
}
self.connection.add_param_headers("Authorization", "Bearer " + token)
data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload)
data_raw = await self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload)
return raise_error_from_response(data_raw, KeycloakPostError)
def has_uma_access(self, token, permissions):
async def has_uma_access(self, token, permissions):
"""Determine whether user has uma permissions with specified user token.
:param token: user token
@ -655,7 +656,7 @@ class KeycloakOpenID:
"""
needed = build_permission_param(permissions)
try:
granted = self.uma_permissions(token, permissions)
granted = await self.uma_permissions(token, permissions)
except (KeycloakPostError, KeycloakAuthenticationError) as e:
if e.response_code == 403: # pragma: no cover
return AuthStatus(

105
tests/conftest.py

@ -6,6 +6,7 @@ import uuid
from datetime import datetime, timedelta
import pytest
import pytest_asyncio
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
@ -134,8 +135,8 @@ def env():
return KeycloakTestEnv()
@pytest.fixture
def admin(env: KeycloakTestEnv):
@pytest_asyncio.fixture
async def admin(env: KeycloakTestEnv):
"""Fixture for initialized KeycloakAdmin class.
:param env: Keycloak test environment
@ -143,15 +144,17 @@ def admin(env: KeycloakTestEnv):
:returns: Keycloak admin
:rtype: KeycloakAdmin
"""
return KeycloakAdmin(
admin = KeycloakAdmin(
server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
username=env.KEYCLOAK_ADMIN,
password=env.KEYCLOAK_ADMIN_PASSWORD,
)
await admin.connect()
return admin
@pytest.fixture
def oid(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin):
@pytest_asyncio.fixture
async def oid(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin):
"""Fixture for initialized KeycloakOpenID class.
:param env: Keycloak test environment
@ -167,7 +170,7 @@ def oid(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin):
admin.realm_name = realm
# Create client
client = str(uuid.uuid4())
client_id = admin.create_client(
client_id = await admin.create_client(
payload={
"name": client,
"clientId": client,
@ -183,11 +186,11 @@ def oid(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin):
client_id=client,
)
# Cleanup
admin.delete_client(client_id=client_id)
await admin.delete_client(client_id=client_id)
@pytest.fixture
def oid_with_credentials(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin):
@pytest_asyncio.fixture
async def oid_with_credentials(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin):
"""Fixture for an initialized KeycloakOpenID class and a random user credentials.
:param env: Keycloak test environment
@ -204,7 +207,7 @@ def oid_with_credentials(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin)
# Create client
client = str(uuid.uuid4())
secret = str(uuid.uuid4())
client_id = admin.create_client(
client_id = await admin.create_client(
payload={
"name": client,
"clientId": client,
@ -218,7 +221,7 @@ def oid_with_credentials(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin)
# Create user
username = str(uuid.uuid4())
password = str(uuid.uuid4())
user_id = admin.create_user(
user_id = await admin.create_user(
payload={
"username": username,
"email": f"{username}@test.test",
@ -239,12 +242,12 @@ def oid_with_credentials(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin)
)
# Cleanup
admin.delete_client(client_id=client_id)
admin.delete_user(user_id=user_id)
await admin.delete_client(client_id=client_id)
await admin.delete_user(user_id=user_id)
@pytest.fixture
def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin):
@pytest_asyncio.fixture
async def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin):
"""Fixture for an initialized KeycloakOpenID class and a random user credentials.
:param env: Keycloak test environment
@ -261,7 +264,7 @@ def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: Keycloak
# Create client
client = str(uuid.uuid4())
secret = str(uuid.uuid4())
client_id = admin.create_client(
client_id = await admin.create_client(
payload={
"name": client,
"clientId": client,
@ -274,17 +277,19 @@ def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: Keycloak
"serviceAccountsEnabled": True,
}
)
admin.create_client_authz_role_based_policy(
role = await admin.get_realm_role(role_name="offline_access")
payload = {
"name": "test-authz-rb-policy",
"roles": [{"id": role["id"]}],
}
await admin.create_client_authz_role_based_policy(
client_id=client_id,
payload={
"name": "test-authz-rb-policy",
"roles": [{"id": admin.get_realm_role(role_name="offline_access")["id"]}],
},
payload=payload,
)
# Create user
username = str(uuid.uuid4())
password = str(uuid.uuid4())
user_id = admin.create_user(
user_id = await admin.create_user(
payload={
"username": username,
"email": f"{username}@test.test",
@ -305,12 +310,12 @@ def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: Keycloak
)
# Cleanup
admin.delete_client(client_id=client_id)
admin.delete_user(user_id=user_id)
await admin.delete_client(client_id=client_id)
await admin.delete_user(user_id=user_id)
@pytest.fixture
def realm(admin: KeycloakAdmin) -> str:
@pytest_asyncio.fixture
async def realm(admin: KeycloakAdmin) -> str:
"""Fixture for a new random realm.
:param admin: Keycloak admin
@ -319,13 +324,13 @@ def realm(admin: KeycloakAdmin) -> str:
:rtype: str
"""
realm_name = str(uuid.uuid4())
admin.create_realm(payload={"realm": realm_name, "enabled": True})
await admin.create_realm(payload={"realm": realm_name, "enabled": True})
yield realm_name
admin.delete_realm(realm_name=realm_name)
await admin.delete_realm(realm_name=realm_name)
@pytest.fixture
def user(admin: KeycloakAdmin, realm: str) -> str:
@pytest_asyncio.fixture
async def user(admin: KeycloakAdmin, realm: str) -> str:
"""Fixture for a new random user.
:param admin: Keycloak admin
@ -337,13 +342,13 @@ def user(admin: KeycloakAdmin, realm: str) -> str:
"""
admin.realm_name = realm
username = str(uuid.uuid4())
user_id = admin.create_user(payload={"username": username, "email": f"{username}@test.test"})
user_id = await admin.create_user(payload={"username": username, "email": f"{username}@test.test"})
yield user_id
admin.delete_user(user_id=user_id)
await admin.delete_user(user_id=user_id)
@pytest.fixture
def group(admin: KeycloakAdmin, realm: str) -> str:
@pytest_asyncio.fixture
async def group(admin: KeycloakAdmin, realm: str) -> str:
"""Fixture for a new random group.
:param admin: Keycloak admin
@ -355,13 +360,13 @@ def group(admin: KeycloakAdmin, realm: str) -> str:
"""
admin.realm_name = realm
group_name = str(uuid.uuid4())
group_id = admin.create_group(payload={"name": group_name})
group_id = await admin.create_group(payload={"name": group_name})
yield group_id
admin.delete_group(group_id=group_id)
await admin.delete_group(group_id=group_id)
@pytest.fixture
def client(admin: KeycloakAdmin, realm: str) -> str:
@pytest_asyncio.fixture
async def client(admin: KeycloakAdmin, realm: str) -> str:
"""Fixture for a new random client.
:param admin: Keycloak admin
@ -373,13 +378,13 @@ def client(admin: KeycloakAdmin, realm: str) -> str:
"""
admin.realm_name = realm
client = str(uuid.uuid4())
client_id = admin.create_client(payload={"name": client, "clientId": client})
client_id = await admin.create_client(payload={"name": client, "clientId": client})
yield client_id
admin.delete_client(client_id=client_id)
await admin.delete_client(client_id=client_id)
@pytest.fixture
def client_role(admin: KeycloakAdmin, realm: str, client: str) -> str:
@pytest_asyncio.fixture
async def client_role(admin: KeycloakAdmin, realm: str, client: str) -> str:
"""Fixture for a new random client role.
:param admin: Keycloak admin
@ -393,13 +398,13 @@ def client_role(admin: KeycloakAdmin, realm: str, client: str) -> str:
"""
admin.realm_name = realm
role = str(uuid.uuid4())
admin.create_client_role(client, {"name": role, "composite": False})
await admin.create_client_role(client, {"name": role, "composite": False})
yield role
admin.delete_client_role(client, role)
await admin.delete_client_role(client, role)
@pytest.fixture
def composite_client_role(admin: KeycloakAdmin, realm: str, client: str, client_role: str) -> str:
@pytest_asyncio.fixture
async def composite_client_role(admin: KeycloakAdmin, realm: str, client: str, client_role: str) -> str:
"""Fixture for a new random composite client role.
:param admin: Keycloak admin
@ -415,11 +420,11 @@ def composite_client_role(admin: KeycloakAdmin, realm: str, client: str, client_
"""
admin.realm_name = realm
role = str(uuid.uuid4())
admin.create_client_role(client, {"name": role, "composite": True})
role_repr = admin.get_client_role(client, client_role)
admin.add_composite_client_roles_to_role(client, role, roles=[role_repr])
await admin.create_client_role(client, {"name": role, "composite": True})
role_repr = await admin.get_client_role(client, client_role)
await admin.add_composite_client_roles_to_role(client, role, roles=[role_repr])
yield role
admin.delete_client_role(client, role)
await admin.delete_client_role(client, role)
@pytest.fixture

1106
tests/test_keycloak_admin.py
File diff suppressed because it is too large
View File

186
tests/test_keycloak_openid.py

@ -40,13 +40,14 @@ def test_keycloak_openid_init(env):
assert isinstance(oid.authorization, Authorization)
def test_well_known(oid: KeycloakOpenID):
@pytest.mark.asyncio
async def test_well_known(oid: KeycloakOpenID):
"""Test the well_known method.
:param oid: Keycloak OpenID client
:type oid: KeycloakOpenID
"""
res = oid.well_known()
res = await oid.well_known()
assert res is not None
assert res != dict()
for key in [
@ -107,7 +108,8 @@ def test_well_known(oid: KeycloakOpenID):
assert key in res
def test_auth_url(env, oid: KeycloakOpenID):
@pytest.mark.asyncio
async def test_auth_url(env, oid: KeycloakOpenID):
"""Test the auth_url method.
:param env: Environment fixture
@ -115,7 +117,7 @@ def test_auth_url(env, oid: KeycloakOpenID):
:param oid: Keycloak OpenID client
:type oid: KeycloakOpenID
"""
res = oid.auth_url(redirect_uri="http://test.test/*")
res = await oid.auth_url(redirect_uri="http://test.test/*")
assert (
res
== f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}/realms/{oid.realm_name}"
@ -124,14 +126,15 @@ def test_auth_url(env, oid: KeycloakOpenID):
)
def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
@pytest.mark.asyncio
async def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
"""Test the token method.
:param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
:type oid_with_credentials: Tuple[KeycloakOpenID, str, str]
"""
oid, username, password = oid_with_credentials
token = oid.token(username=username, password=password)
token = await oid.token(username=username, password=password)
assert token == {
"access_token": mock.ANY,
"expires_in": 300,
@ -145,7 +148,7 @@ def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
}
# Test with dummy totp
token = oid.token(username=username, password=password, totp="123456")
token = await oid.token(username=username, password=password, totp="123456")
assert token == {
"access_token": mock.ANY,
"expires_in": 300,
@ -159,7 +162,7 @@ def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
}
# Test with extra param
token = oid.token(username=username, password=password, extra_param="foo")
token = await oid.token(username=username, password=password, extra_param="foo")
assert token == {
"access_token": mock.ANY,
"expires_in": 300,
@ -173,7 +176,8 @@ def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
}
def test_exchange_token(
@pytest.mark.asyncio
async def test_exchange_token(
oid_with_credentials: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin
):
"""Test the exchange token method.
@ -188,19 +192,23 @@ def test_exchange_token(
# Allow impersonation
admin.realm_name = oid.realm_name
admin.assign_client_role(
user_id=admin.get_user_id(username=username),
client_id=admin.get_client_id(client_id="realm-management"),
roles=[
admin.get_client_role(
client_id=admin.get_client_id(client_id="realm-management"),
role_name="impersonation",
)
],
user_id = await admin.get_user_id(username=username)
client_id = await admin.get_client_id(client_id="realm-management")
roles = [
await admin.get_client_role(
client_id=client_id,
role_name="impersonation",
)
]
print(roles)
await admin.assign_client_role(
user_id=user_id,
client_id=client_id,
roles=roles
)
token = oid.token(username=username, password=password)
assert oid.userinfo(token=token["access_token"]) == {
token = await oid.token(username=username, password=password)
assert await oid.userinfo(token=token["access_token"]) == {
"email": f"{username}@test.test",
"email_verified": False,
"preferred_username": username,
@ -208,13 +216,13 @@ def test_exchange_token(
}
# Exchange token with the new user
new_token = oid.exchange_token(
new_token = await oid.exchange_token(
token=token["access_token"],
client_id=oid.client_id,
audience=oid.client_id,
subject=username,
)
assert oid.userinfo(token=new_token["access_token"]) == {
assert await oid.userinfo(token=new_token["access_token"]) == {
"email": f"{username}@test.test",
"email_verified": False,
"preferred_username": username,
@ -223,7 +231,8 @@ def test_exchange_token(
assert token != new_token
def test_logout(oid_with_credentials):
@pytest.mark.asyncio
async def test_logout(oid_with_credentials):
"""Test logout.
:param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
@ -231,33 +240,37 @@ def test_logout(oid_with_credentials):
"""
oid, username, password = oid_with_credentials
token = oid.token(username=username, password=password)
assert oid.userinfo(token=token["access_token"]) != dict()
assert oid.logout(refresh_token=token["refresh_token"]) == dict()
token = await oid.token(username=username, password=password)
assert await oid.userinfo(token=token["access_token"]) != dict()
assert await oid.logout(refresh_token=token["refresh_token"]) == dict()
with pytest.raises(KeycloakAuthenticationError):
oid.userinfo(token=token["access_token"])
await oid.userinfo(token=token["access_token"])
def test_certs(oid: KeycloakOpenID):
@pytest.mark.asyncio
async def test_certs(oid: KeycloakOpenID):
"""Test certificates.
:param oid: Keycloak OpenID client
:type oid: KeycloakOpenID
"""
assert len(oid.certs()["keys"]) == 2
certs = await oid.certs()
assert len(certs["keys"]) == 2
def test_public_key(oid: KeycloakOpenID):
@pytest.mark.asyncio
async def test_public_key(oid: KeycloakOpenID):
"""Test public key.
:param oid: Keycloak OpenID client
:type oid: KeycloakOpenID
"""
assert oid.public_key() is not None
assert await oid.public_key() is not None
def test_entitlement(
@pytest.mark.asyncio
async def test_entitlement(
oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin
):
"""Test entitlement.
@ -269,53 +282,61 @@ def test_entitlement(
:type admin: KeycloakAdmin
"""
oid, username, password = oid_with_credentials_authz
token = oid.token(username=username, password=password)
resource_server_id = admin.get_client_authz_resources(
client_id=admin.get_client_id(oid.client_id)
)[0]["_id"]
token = await oid.token(username=username, password=password)
client_id = await admin.get_client_id(oid.client_id)
with pytest.raises(KeycloakDeprecationError):
resource_servers = await admin.get_client_authz_resources(
client_id=client_id
)
resource_server_id = resource_servers[0]["_id"]
oid.entitlement(token=token["access_token"], resource_server_id=resource_server_id)
def test_introspect(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
@pytest.mark.asyncio
async def test_introspect(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
"""Test introspect.
:param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
:type oid_with_credentials: Tuple[KeycloakOpenID, str, str]
"""
oid, username, password = oid_with_credentials
token = oid.token(username=username, password=password)
token = await oid.token(username=username, password=password)
assert oid.introspect(token=token["access_token"])["active"]
assert oid.introspect(
introspect = await oid.introspect(token=token["access_token"])
assert introspect["active"]
introspect = await oid.introspect(
token=token["access_token"], rpt="some", token_type_hint="requesting_party_token"
) == {"active": False}
)
assert introspect == {"active": False}
with pytest.raises(KeycloakRPTNotFound):
oid.introspect(token=token["access_token"], token_type_hint="requesting_party_token")
await oid.introspect(token=token["access_token"], token_type_hint="requesting_party_token")
def test_decode_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
@pytest.mark.asyncio
async def test_decode_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
"""Test decode token.
:param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
:type oid_with_credentials: Tuple[KeycloakOpenID, str, str]
"""
oid, username, password = oid_with_credentials
token = oid.token(username=username, password=password)
token = await oid.token(username=username, password=password)
decoded_token = await oid.decode_token(
token=token["access_token"],
key="-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----",
options={"verify_aud": False},
)
assert (
oid.decode_token(
token=token["access_token"],
key="-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----",
options={"verify_aud": False},
)["preferred_username"]
decoded_token["preferred_username"]
== username
)
def test_load_authorization_config(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
@pytest.mark.asyncio
async def test_load_authorization_config(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
"""Test load authorization config.
:param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
@ -335,7 +356,8 @@ def test_load_authorization_config(oid_with_credentials_authz: Tuple[KeycloakOpe
)
def test_get_policies(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
@pytest.mark.asyncio
async def test_get_policies(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
"""Test get policies.
:param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
@ -343,37 +365,38 @@ def test_get_policies(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str
:type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
"""
oid, username, password = oid_with_credentials_authz
token = oid.token(username=username, password=password)
token = await oid.token(username=username, password=password)
with pytest.raises(KeycloakAuthorizationConfigError):
oid.get_policies(token=token["access_token"])
await oid.get_policies(token=token["access_token"])
oid.load_authorization_config(path="tests/data/authz_settings.json")
assert oid.get_policies(token=token["access_token"]) is None
assert await oid.get_policies(token=token["access_token"]) is None
key = "-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----"
orig_client_id = oid.client_id
oid.client_id = "account"
assert oid.get_policies(token=token["access_token"], method_token_info="decode", key=key) == []
assert await oid.get_policies(token=token["access_token"], method_token_info="decode", key=key) == []
policy = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS")
policy.add_role(role="account/view-profile")
oid.authorization.policies["test"] = policy
assert [
str(x)
for x in oid.get_policies(token=token["access_token"], method_token_info="decode", key=key)
for x in await oid.get_policies(token=token["access_token"], method_token_info="decode", key=key)
] == ["Policy: test (role)"]
assert [
repr(x)
for x in oid.get_policies(token=token["access_token"], method_token_info="decode", key=key)
for x in await oid.get_policies(token=token["access_token"], method_token_info="decode", key=key)
] == ["<Policy: test (role)>"]
oid.client_id = orig_client_id
oid.logout(refresh_token=token["refresh_token"])
with pytest.raises(KeycloakInvalidTokenError):
oid.get_policies(token=token["access_token"])
await oid.get_policies(token=token["access_token"])
def test_get_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
@pytest.mark.asyncio
async def test_get_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
"""Test get policies.
:param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
@ -381,19 +404,19 @@ def test_get_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str,
:type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
"""
oid, username, password = oid_with_credentials_authz
token = oid.token(username=username, password=password)
token = await oid.token(username=username, password=password)
with pytest.raises(KeycloakAuthorizationConfigError):
oid.get_permissions(token=token["access_token"])
await oid.get_permissions(token=token["access_token"])
oid.load_authorization_config(path="tests/data/authz_settings.json")
assert oid.get_permissions(token=token["access_token"]) is None
assert await oid.get_permissions(token=token["access_token"]) is None
key = "-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----"
orig_client_id = oid.client_id
oid.client_id = "account"
assert (
oid.get_permissions(token=token["access_token"], method_token_info="decode", key=key) == []
await oid.get_permissions(token=token["access_token"], method_token_info="decode", key=key) == []
)
policy = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS")
policy.add_role(role="account/view-profile")
@ -405,24 +428,25 @@ def test_get_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str,
oid.authorization.policies["test"] = policy
assert [
str(x)
for x in oid.get_permissions(
for x in await oid.get_permissions(
token=token["access_token"], method_token_info="decode", key=key
)
] == ["Permission: test-perm (resource)"]
assert [
repr(x)
for x in oid.get_permissions(
for x in await oid.get_permissions(
token=token["access_token"], method_token_info="decode", key=key
)
] == ["<Permission: test-perm (resource)>"]
oid.client_id = orig_client_id
oid.logout(refresh_token=token["refresh_token"])
await oid.logout(refresh_token=token["refresh_token"])
with pytest.raises(KeycloakInvalidTokenError):
oid.get_permissions(token=token["access_token"])
await oid.get_permissions(token=token["access_token"])
def test_uma_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
@pytest.mark.asyncio
async def test_uma_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
"""Test UMA permissions.
:param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
@ -430,13 +454,15 @@ def test_uma_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str,
:type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
"""
oid, username, password = oid_with_credentials_authz
token = oid.token(username=username, password=password)
token = await oid.token(username=username, password=password)
assert len(oid.uma_permissions(token=token["access_token"])) == 1
assert oid.uma_permissions(token=token["access_token"])[0]["rsname"] == "Default Resource"
assert len(await oid.uma_permissions(token=token["access_token"])) == 1
uma_permissions = await oid.uma_permissions(token=token["access_token"])
assert uma_permissions[0]["rsname"] == "Default Resource"
def test_has_uma_access(
@pytest.mark.asyncio
async def test_has_uma_access(
oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin
):
"""Test has UMA access.
@ -448,27 +474,27 @@ def test_has_uma_access(
:type admin: KeycloakAdmin
"""
oid, username, password = oid_with_credentials_authz
token = oid.token(username=username, password=password)
token = await oid.token(username=username, password=password)
assert (
str(oid.has_uma_access(token=token["access_token"], permissions=""))
str(await oid.has_uma_access(token=token["access_token"], permissions=""))
== "AuthStatus(is_authorized=True, is_logged_in=True, missing_permissions=set())"
)
assert (
str(oid.has_uma_access(token=token["access_token"], permissions="Default Resource"))
str(await oid.has_uma_access(token=token["access_token"], permissions="Default Resource"))
== "AuthStatus(is_authorized=True, is_logged_in=True, missing_permissions=set())"
)
with pytest.raises(KeycloakPostError):
oid.has_uma_access(token=token["access_token"], permissions="Does not exist")
await oid.has_uma_access(token=token["access_token"], permissions="Does not exist")
oid.logout(refresh_token=token["refresh_token"])
await oid.logout(refresh_token=token["refresh_token"])
assert (
str(oid.has_uma_access(token=token["access_token"], permissions=""))
str(await oid.has_uma_access(token=token["access_token"], permissions=""))
== "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions=set())"
)
assert (
str(oid.has_uma_access(token=admin.token["access_token"], permissions="Default Resource"))
str(await oid.has_uma_access(token=admin.token["access_token"], permissions="Default Resource"))
== "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions="
+ "{'Default Resource'})"
)

34
tox.ini

@ -9,23 +9,23 @@ envlist = check, apply-check, docs, tests, build, changelog
whitelist_externals =
bash
[testenv:check]
commands =
black --check --diff src/keycloak tests docs
isort -c --df src/keycloak tests docs
flake8 src/keycloak tests docs
codespell src tests docs
[testenv:apply-check]
commands =
black -C src/keycloak tests docs
black src/keycloak tests docs
isort src/keycloak tests docs
[testenv:docs]
extras = docs
commands =
sphinx-build -T -E -W -b html -d _build/doctrees -D language=en ./docs/source _build/html
#[testenv:check]
#commands =
# black --check --diff src/keycloak tests docs
# isort -c --df src/keycloak tests docs
# flake8 src/keycloak tests docs
# codespell src tests docs
#[testenv:apply-check]
#commands =
# black -C src/keycloak tests docs
# black src/keycloak tests docs
# isort src/keycloak tests docs
#[testenv:docs]
#extras = docs
#commands =
# sphinx-build -T -E -W -b html -d _build/doctrees -D language=en ./docs/source _build/html
[testenv:tests]
setenv = file|tox.env

Loading…
Cancel
Save