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. 103
      tests/conftest.py
  9. 1106
      tests/test_keycloak_admin.py
  10. 176
      tests/test_keycloak_openid.py
  11. 34
      tox.ini

2
CHANGELOG.md

@ -1,3 +1,5 @@
## Unreleased
## v2.9.0 (2023-01-11) ## v2.9.0 (2023-01-11)
### Feat ### 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] [tool.poetry.dependencies]
python = "^3.7" python = "^3.7"
requests = "^2.20.0"
httpx = "^0.23.0"
python-jose = "^3.3.0" python-jose = "^3.3.0"
urllib3 = "^1.26.0" urllib3 = "^1.26.0"
mock = {version = "^4.0.3", optional = true} 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} readthedocs-sphinx-ext = {version = "^2.1.9", optional = true}
m2r2 = {version = "^0.3.2", optional = true} m2r2 = {version = "^0.3.2", optional = true}
sphinx-autoapi = {version = "^2.0.0", optional = true} sphinx-autoapi = {version = "^2.0.0", optional = true}
requests-toolbelt = "^0.9.1"
[tool.poetry.extras] [tool.poetry.extras]
docs = [ docs = [
@ -61,6 +60,7 @@ docs = [
tox = "^3.25.0" tox = "^3.25.0"
pytest = "^7.1.2" pytest = "^7.1.2"
pytest-cov = "^3.0.0" pytest-cov = "^3.0.0"
pytest-asyncio = "0.20.3"
wheel = "^0.37.1" wheel = "^0.37.1"
pre-commit = "^2.19.0" pre-commit = "^2.19.0"
isort = "^5.10.1" isort = "^5.10.1"

40
src/keycloak/connection.py

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

4
src/keycloak/exceptions.py

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

103
tests/conftest.py

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

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

176
tests/test_keycloak_openid.py

@ -40,13 +40,14 @@ def test_keycloak_openid_init(env):
assert isinstance(oid.authorization, Authorization) 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. """Test the well_known method.
:param oid: Keycloak OpenID client :param oid: Keycloak OpenID client
:type oid: KeycloakOpenID :type oid: KeycloakOpenID
""" """
res = oid.well_known()
res = await oid.well_known()
assert res is not None assert res is not None
assert res != dict() assert res != dict()
for key in [ for key in [
@ -107,7 +108,8 @@ def test_well_known(oid: KeycloakOpenID):
assert key in res 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. """Test the auth_url method.
:param env: Environment fixture :param env: Environment fixture
@ -115,7 +117,7 @@ def test_auth_url(env, oid: KeycloakOpenID):
:param oid: Keycloak OpenID client :param oid: Keycloak OpenID client
:type oid: KeycloakOpenID :type oid: KeycloakOpenID
""" """
res = oid.auth_url(redirect_uri="http://test.test/*")
res = await oid.auth_url(redirect_uri="http://test.test/*")
assert ( assert (
res res
== f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}/realms/{oid.realm_name}" == 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. """Test the token method.
:param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
:type oid_with_credentials: Tuple[KeycloakOpenID, str, str] :type oid_with_credentials: Tuple[KeycloakOpenID, str, str]
""" """
oid, username, password = oid_with_credentials oid, username, password = oid_with_credentials
token = oid.token(username=username, password=password)
token = await oid.token(username=username, password=password)
assert token == { assert token == {
"access_token": mock.ANY, "access_token": mock.ANY,
"expires_in": 300, "expires_in": 300,
@ -145,7 +148,7 @@ def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
} }
# Test with dummy totp # 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 == { assert token == {
"access_token": mock.ANY, "access_token": mock.ANY,
"expires_in": 300, "expires_in": 300,
@ -159,7 +162,7 @@ def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
} }
# Test with extra param # 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 == { assert token == {
"access_token": mock.ANY, "access_token": mock.ANY,
"expires_in": 300, "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 oid_with_credentials: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin
): ):
"""Test the exchange token method. """Test the exchange token method.
@ -188,19 +192,23 @@ def test_exchange_token(
# Allow impersonation # Allow impersonation
admin.realm_name = oid.realm_name 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"),
user_id = await admin.get_user_id(username=username)
client_id = await admin.get_client_id(client_id="realm-management")
roles = [ roles = [
admin.get_client_role(
client_id=admin.get_client_id(client_id="realm-management"),
await admin.get_client_role(
client_id=client_id,
role_name="impersonation", 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": f"{username}@test.test",
"email_verified": False, "email_verified": False,
"preferred_username": username, "preferred_username": username,
@ -208,13 +216,13 @@ def test_exchange_token(
} }
# Exchange token with the new user # Exchange token with the new user
new_token = oid.exchange_token(
new_token = await oid.exchange_token(
token=token["access_token"], token=token["access_token"],
client_id=oid.client_id, client_id=oid.client_id,
audience=oid.client_id, audience=oid.client_id,
subject=username, 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": f"{username}@test.test",
"email_verified": False, "email_verified": False,
"preferred_username": username, "preferred_username": username,
@ -223,7 +231,8 @@ def test_exchange_token(
assert token != new_token assert token != new_token
def test_logout(oid_with_credentials):
@pytest.mark.asyncio
async def test_logout(oid_with_credentials):
"""Test logout. """Test logout.
:param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials :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 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): 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. """Test certificates.
:param oid: Keycloak OpenID client :param oid: Keycloak OpenID client
:type oid: KeycloakOpenID :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. """Test public key.
:param oid: Keycloak OpenID client :param oid: Keycloak OpenID client
:type oid: KeycloakOpenID :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 oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin
): ):
"""Test entitlement. """Test entitlement.
@ -269,53 +282,61 @@ def test_entitlement(
:type admin: KeycloakAdmin :type admin: KeycloakAdmin
""" """
oid, username, password = oid_with_credentials_authz 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): 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) 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. """Test introspect.
:param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
:type oid_with_credentials: Tuple[KeycloakOpenID, str, str] :type oid_with_credentials: Tuple[KeycloakOpenID, str, str]
""" """
oid, username, password = oid_with_credentials oid, username, password = oid_with_credentials
token = oid.token(username=username, password=password)
token = await oid.token(username=username, password=password)
introspect = await oid.introspect(token=token["access_token"])
assert introspect["active"]
assert oid.introspect(token=token["access_token"])["active"]
assert oid.introspect(
introspect = await oid.introspect(
token=token["access_token"], rpt="some", token_type_hint="requesting_party_token" token=token["access_token"], rpt="some", token_type_hint="requesting_party_token"
) == {"active": False}
)
assert introspect == {"active": False}
with pytest.raises(KeycloakRPTNotFound): 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. """Test decode token.
:param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
:type oid_with_credentials: Tuple[KeycloakOpenID, str, str] :type oid_with_credentials: Tuple[KeycloakOpenID, str, str]
""" """
oid, username, password = oid_with_credentials oid, username, password = oid_with_credentials
token = oid.token(username=username, password=password)
token = await oid.token(username=username, password=password)
assert (
oid.decode_token(
decoded_token = await oid.decode_token(
token=token["access_token"], token=token["access_token"],
key="-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----", key="-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----",
options={"verify_aud": False}, options={"verify_aud": False},
)["preferred_username"]
)
assert (
decoded_token["preferred_username"]
== 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. """Test load authorization config.
:param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization :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. """Test get policies.
:param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization :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] :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
""" """
oid, username, password = oid_with_credentials_authz 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): 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") 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-----" key = "-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----"
orig_client_id = oid.client_id orig_client_id = oid.client_id
oid.client_id = "account" 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 = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS")
policy.add_role(role="account/view-profile") policy.add_role(role="account/view-profile")
oid.authorization.policies["test"] = policy oid.authorization.policies["test"] = policy
assert [ assert [
str(x) 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)"] ] == ["Policy: test (role)"]
assert [ assert [
repr(x) 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)>"] ] == ["<Policy: test (role)>"]
oid.client_id = orig_client_id oid.client_id = orig_client_id
oid.logout(refresh_token=token["refresh_token"]) oid.logout(refresh_token=token["refresh_token"])
with pytest.raises(KeycloakInvalidTokenError): 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. """Test get policies.
:param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization :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] :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
""" """
oid, username, password = oid_with_credentials_authz 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): 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") 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-----" key = "-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----"
orig_client_id = oid.client_id orig_client_id = oid.client_id
oid.client_id = "account" oid.client_id = "account"
assert ( 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 = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS")
policy.add_role(role="account/view-profile") 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 oid.authorization.policies["test"] = policy
assert [ assert [
str(x) 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 token=token["access_token"], method_token_info="decode", key=key
) )
] == ["Permission: test-perm (resource)"] ] == ["Permission: test-perm (resource)"]
assert [ assert [
repr(x) 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 token=token["access_token"], method_token_info="decode", key=key
) )
] == ["<Permission: test-perm (resource)>"] ] == ["<Permission: test-perm (resource)>"]
oid.client_id = orig_client_id 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): 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. """Test UMA permissions.
:param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization :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] :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
""" """
oid, username, password = oid_with_credentials_authz 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 oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin
): ):
"""Test has UMA access. """Test has UMA access.
@ -448,27 +474,27 @@ def test_has_uma_access(
:type admin: KeycloakAdmin :type admin: KeycloakAdmin
""" """
oid, username, password = oid_with_credentials_authz oid, username, password = oid_with_credentials_authz
token = oid.token(username=username, password=password)
token = await oid.token(username=username, password=password)
assert ( 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())" == "AuthStatus(is_authorized=True, is_logged_in=True, missing_permissions=set())"
) )
assert ( 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())" == "AuthStatus(is_authorized=True, is_logged_in=True, missing_permissions=set())"
) )
with pytest.raises(KeycloakPostError): 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 ( 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())" == "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions=set())"
) )
assert ( 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=" == "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions="
+ "{'Default Resource'})" + "{'Default Resource'})"
) )

34
tox.ini

@ -9,23 +9,23 @@ envlist = check, apply-check, docs, tests, build, changelog
whitelist_externals = whitelist_externals =
bash 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] [testenv:tests]
setenv = file|tox.env setenv = file|tox.env

Loading…
Cancel
Save