From b95b1d3505d5fe5d7f82ebb871122ca9afd80d6e Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 09:19:12 +0200 Subject: [PATCH 01/19] refactor: slight restructure of the base fixtures --- tests/conftest.py | 73 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b9c266a..ada9820 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,22 +3,62 @@ import uuid import pytest -from keycloak import KeycloakAdmin +from keycloak import KeycloakAdmin, KeycloakOpenID + + +class KeycloakTestEnv(object): + def __init__( + self, + host: str = os.environ["KEYCLOAK_HOST"], + port: str = os.environ["KEYCLOAK_PORT"], + username: str = os.environ["KEYCLOAK_ADMIN"], + password: str = os.environ["KEYCLOAK_ADMIN_PASSWORD"], + ): + self.KEYCLOAK_HOST = host + self.KEYCLOAK_PORT = port + self.KEYCLOAK_ADMIN = username + self.KEYCLOAK_ADMIN_PASSWORD = password + + @property + def KEYCLOAK_HOST(self): + return self._KEYCLOAK_HOST + + @KEYCLOAK_HOST.setter + def KEYCLOAK_HOST(self, value: str): + self._KEYCLOAK_HOST = value + + @property + def KEYCLOAK_PORT(self): + return self._KEYCLOAK_PORT + + @KEYCLOAK_PORT.setter + def KEYCLOAK_PORT(self, value: str): + self._KEYCLOAK_PORT = value + + @property + def KEYCLOAK_ADMIN(self): + return self._KEYCLOAK_ADMIN + + @KEYCLOAK_ADMIN.setter + def KEYCLOAK_ADMIN(self, value: str): + self._KEYCLOAK_ADMIN = value + + @property + def KEYCLOAK_ADMIN_PASSWORD(self): + return self._KEYCLOAK_ADMIN_PASSWORD + + @KEYCLOAK_ADMIN_PASSWORD.setter + def KEYCLOAK_ADMIN_PASSWORD(self, value: str): + self._KEYCLOAK_ADMIN_PASSWORD = value @pytest.fixture def env(): - class KeycloakTestEnv(object): - KEYCLOAK_HOST = os.environ["KEYCLOAK_HOST"] - KEYCLOAK_PORT = os.environ["KEYCLOAK_PORT"] - KEYCLOAK_ADMIN = os.environ["KEYCLOAK_ADMIN"] - KEYCLOAK_ADMIN_PASSWORD = os.environ["KEYCLOAK_ADMIN_PASSWORD"] - return KeycloakTestEnv() @pytest.fixture -def admin(env): +def admin(env: KeycloakTestEnv): return KeycloakAdmin( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", username=env.KEYCLOAK_ADMIN, @@ -26,6 +66,23 @@ def admin(env): ) +@pytest.fixture +def oid(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin): + # Set the realm + admin.realm_name = realm + # Create client + client = str(uuid.uuid4()) + client_id = admin.create_client(payload={"name": client, "clientId": client}) + # Return OID + yield KeycloakOpenID( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + realm_name=realm, + client_id=client, + ) + # Cleanup + admin.delete_client(client_id=client_id) + + @pytest.fixture def realm(admin: KeycloakAdmin) -> str: realm_name = str(uuid.uuid4()) From 590c7bb582237739582fc58352048ad3ca27be84 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 09:19:38 +0200 Subject: [PATCH 02/19] test: test of init and well_known of oid --- src/keycloak/keycloak_openid.py | 1 - tests/test_keycloak_openid.py | 79 +++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/test_keycloak_openid.py diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 7216b5d..3e045bc 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -170,7 +170,6 @@ class KeycloakOpenID: params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_get(URL_WELL_KNOWN.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError) def auth_url(self, redirect_uri): diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py new file mode 100644 index 0000000..0ee1bbb --- /dev/null +++ b/tests/test_keycloak_openid.py @@ -0,0 +1,79 @@ +from keycloak.keycloak_openid import KeycloakOpenID +from keycloak.connection import ConnectionManager +from keycloak.authorization import Authorization + + +def test_keycloak_openid_init(env): + oid = KeycloakOpenID( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + realm_name="master", + client_id="admin-cli", + ) + + assert oid.client_id == "admin-cli" + assert oid.client_secret_key is None + assert oid.realm_name == "master" + assert isinstance(oid.connection, ConnectionManager) + assert isinstance(oid.authorization, Authorization) + + +def test_well_known(oid: KeycloakOpenID): + res = oid.well_known() + assert res is not None + assert res != dict() + for key in [ + "acr_values_supported", + "authorization_encryption_alg_values_supported", + "authorization_encryption_enc_values_supported", + "authorization_endpoint", + "authorization_signing_alg_values_supported", + "backchannel_authentication_endpoint", + "backchannel_authentication_request_signing_alg_values_supported", + "backchannel_logout_session_supported", + "backchannel_logout_supported", + "backchannel_token_delivery_modes_supported", + "check_session_iframe", + "claim_types_supported", + "claims_parameter_supported", + "claims_supported", + "code_challenge_methods_supported", + "device_authorization_endpoint", + "end_session_endpoint", + "frontchannel_logout_session_supported", + "frontchannel_logout_supported", + "grant_types_supported", + "id_token_encryption_alg_values_supported", + "id_token_encryption_enc_values_supported", + "id_token_signing_alg_values_supported", + "introspection_endpoint", + "introspection_endpoint_auth_methods_supported", + "introspection_endpoint_auth_signing_alg_values_supported", + "issuer", + "jwks_uri", + "mtls_endpoint_aliases", + "pushed_authorization_request_endpoint", + "registration_endpoint", + "request_object_encryption_alg_values_supported", + "request_object_encryption_enc_values_supported", + "request_object_signing_alg_values_supported", + "request_parameter_supported", + "request_uri_parameter_supported", + "require_pushed_authorization_requests", + "require_request_uri_registration", + "response_modes_supported", + "response_types_supported", + "revocation_endpoint", + "revocation_endpoint_auth_methods_supported", + "revocation_endpoint_auth_signing_alg_values_supported", + "scopes_supported", + "subject_types_supported", + "tls_client_certificate_bound_access_tokens", + "token_endpoint", + "token_endpoint_auth_methods_supported", + "token_endpoint_auth_signing_alg_values_supported", + "userinfo_encryption_alg_values_supported", + "userinfo_encryption_enc_values_supported", + "userinfo_endpoint", + "userinfo_signing_alg_values_supported", + ]: + assert key in res From d79939535f7da6f8d1db1f37dfc6c3423b108ff4 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 09:20:02 +0200 Subject: [PATCH 03/19] style: applied isort --- tests/test_keycloak_openid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 0ee1bbb..53a76dd 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -1,6 +1,6 @@ -from keycloak.keycloak_openid import KeycloakOpenID -from keycloak.connection import ConnectionManager from keycloak.authorization import Authorization +from keycloak.connection import ConnectionManager +from keycloak.keycloak_openid import KeycloakOpenID def test_keycloak_openid_init(env): From 5cd8fc391398762524e738d434b0bedee1af44cd Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 07:52:23 +0000 Subject: [PATCH 04/19] test: use tox-poetry plugin for tox --- tox.ini | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tox.ini b/tox.ini index b88069a..0458821 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,10 @@ [tox] -isolated_build = true +requires = + tox-poetry + poetry envlist = check, apply-check, docs, tests, build [testenv] -install_command = pip install {opts} {packages} -deps = - poetry>=1.1.13 -commands_pre = - bash -c "python -m pip install -r <(poetry export --dev --extras=docs --without-hashes --no-interaction)" whitelist_externals = bash @@ -24,16 +21,19 @@ commands = 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 +passenv = CONTAINER_HOST commands = ./test_keycloak_init.sh "pytest -vv --cov=keycloak --cov-report term-missing {posargs}" [testenv:build] -commands_pre = +deps = + poetry setenv = POETRY_VIRTUALENVS_CREATE = false commands = From db888185c3b82a8e9581fb2ee3351e698636c63b Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 19:57:18 +0000 Subject: [PATCH 05/19] test: store keycloak container logs --- .github/workflows/lint.yaml | 3 +++ .gitignore | 1 + test_keycloak_init.sh | 3 +-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 2cade19..0f5b146 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -71,6 +71,9 @@ jobs: - name: Run tests run: | tox -e tests + - name: Keycloak logs + run: | + cat keycloak_test_logs.txt build: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 0c17079..25a3ea4 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ nosetests.xml coverage.xml *.cover .hypothesis/ +keycloak_test_logs.txt # Translations *.mo diff --git a/test_keycloak_init.sh b/test_keycloak_init.sh index bd4c30a..82afabb 100755 --- a/test_keycloak_init.sh +++ b/test_keycloak_init.sh @@ -3,8 +3,6 @@ CMD_ARGS=$1 KEYCLOAK_DOCKER_IMAGE="quay.io/keycloak/keycloak:latest" -echo "${CMD_ARGS}" - function keycloak_stop() { docker stop unittest_keycloak &> /dev/null docker rm unittest_keycloak &> /dev/null @@ -30,6 +28,7 @@ keycloak_stop # In case it did not shut down correctly last time. keycloak_start eval ${CMD_ARGS} +docker logs unittest_keycloak > keycloak_test_logs.txt RETURN_VALUE=$? exit ${RETURN_VALUE} From 9ab340f4a42ee2ef8886722873c87992376e01c7 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 19:58:20 +0000 Subject: [PATCH 06/19] feat: added flake8-docstrings and upgraded dependencies --- poetry.lock | 256 +++++++++++++++++++++++++++++++++++++++---------- pyproject.toml | 2 + tox.ini | 2 + 3 files changed, 210 insertions(+), 50 deletions(-) diff --git a/poetry.lock b/poetry.lock index cbad183..c747f94 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,6 +6,20 @@ category = "main" optional = true python-versions = "*" +[[package]] +name = "argcomplete" +version = "1.12.3" +description = "Bash tab completion for argparse" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +importlib-metadata = {version = ">=0.23,<5", markers = "python_version == \"3.7\""} + +[package.extras] +test = ["coverage", "flake8", "pexpect", "wheel"] + [[package]] name = "astroid" version = "2.11.6" @@ -55,7 +69,7 @@ pytz = ">=2015.7" [[package]] name = "black" -version = "22.3.0" +version = "22.6.0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -66,7 +80,7 @@ click = ">=8.0.0" mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} @@ -94,11 +108,11 @@ python-versions = ">=3.6.1" [[package]] name = "charset-normalizer" -version = "2.0.12" +version = "2.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=3.5.0" +python-versions = ">=3.6.0" [package.extras] unicode_backport = ["unicodedata2"] @@ -123,6 +137,26 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "commitizen" +version = "2.28.0" +description = "Python commitizen client tool" +category = "dev" +optional = false +python-versions = ">=3.6.2,<4.0.0" + +[package.dependencies] +argcomplete = ">=1.12.1,<2.0.0" +colorama = ">=0.4.1,<0.5.0" +decli = ">=0.5.2,<0.6.0" +jinja2 = ">=2.10.3" +packaging = ">=19,<22" +pyyaml = ">=3.08" +questionary = ">=1.4.0,<2.0.0" +termcolor = ">=1.1,<2.0" +tomlkit = ">=0.5.3,<1.0.0" +typing-extensions = ">=4.0.1,<5.0.0" + [[package]] name = "commonmark" version = "0.9.1" @@ -148,6 +182,14 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "decli" +version = "0.5.2" +description = "Minimal, easy-to-use, declarative cli tool" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "distlib" version = "0.3.4" @@ -205,6 +247,18 @@ mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.7.0,<2.8.0" pyflakes = ">=2.3.0,<2.4.0" +[[package]] +name = "flake8-docstrings" +version = "1.6.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = ">=3" +pydocstyle = ">=2.1" + [[package]] name = "identify" version = "2.5.1" @@ -226,7 +280,7 @@ python-versions = ">=3.5" [[package]] name = "imagesize" -version = "1.3.0" +version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" category = "main" optional = true @@ -276,7 +330,7 @@ name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." category = "main" -optional = true +optional = false python-versions = ">=3.7" [package.dependencies] @@ -310,7 +364,7 @@ name = "markupsafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" -optional = true +optional = false python-versions = ">=3.7" [[package]] @@ -421,6 +475,17 @@ pyyaml = ">=5.1" toml = "*" virtualenv = ">=20.0.8" +[[package]] +name = "prompt-toolkit" +version = "3.0.30" +description = "Library for building powerful interactive command lines in Python" +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +wcwidth = "*" + [[package]] name = "py" version = "1.11.0" @@ -445,6 +510,20 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pydocstyle" +version = "6.1.1" +description = "Python docstring style checker" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +snowballstemmer = "*" + +[package.extras] +toml = ["toml"] + [[package]] name = "pyflakes" version = "2.3.1" @@ -543,6 +622,20 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "questionary" +version = "1.10.0" +description = "Python library to build pretty command line user prompts ⭐️" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +prompt_toolkit = ">=2.0,<4.0" + +[package.extras] +docs = ["Sphinx (>=3.3,<4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)"] + [[package]] name = "readthedocs-sphinx-ext" version = "2.1.8" @@ -571,7 +664,7 @@ sphinx = ">=1.3.1" [[package]] name = "requests" -version = "2.28.0" +version = "2.28.1" description = "Python HTTP for Humans." category = "main" optional = false @@ -579,13 +672,13 @@ python-versions = ">=3.7, <4" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2.0.0,<2.1.0" +charset-normalizer = ">=2,<3" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rsa" @@ -611,7 +704,7 @@ name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." category = "main" -optional = true +optional = false python-versions = "*" [[package]] @@ -752,6 +845,14 @@ python-versions = ">=3.5" lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +[[package]] +name = "termcolor" +version = "1.1.0" +description = "ANSII Color formatting for output in terminal." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "toml" version = "0.10.2" @@ -768,9 +869,17 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "tomlkit" +version = "0.11.0" +description = "Style preserving TOML library" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + [[package]] name = "tox" -version = "3.25.0" +version = "3.25.1" description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false @@ -801,7 +910,7 @@ python-versions = ">=3.6" [[package]] name = "typing-extensions" -version = "4.2.0" +version = "4.3.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false @@ -830,7 +939,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.15.0" +version = "20.15.1" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -847,6 +956,14 @@ six = ">=1.9.0,<2" docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "wrapt" version = "1.14.1" @@ -873,13 +990,17 @@ docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "58ad1dfa1c2cdbb232bc53ceb2c1a9d0767a3db7fd8e6d0baae3e753f1c570dc" +content-hash = "ed105f41fc20e390af8eeefafd3168bb4b370d3a5135bfdec55aab7fc5d0bb3e" [metadata.files] alabaster = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] +argcomplete = [ + {file = "argcomplete-1.12.3-py2.py3-none-any.whl", hash = "sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81"}, + {file = "argcomplete-1.12.3.tar.gz", hash = "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445"}, +] astroid = [ {file = "astroid-2.11.6-py3-none-any.whl", hash = "sha256:ba33a82a9a9c06a5ceed98180c5aab16e29c285b828d94696bf32d6015ea82a9"}, {file = "astroid-2.11.6.tar.gz", hash = "sha256:4f933d0bf5e408b03a6feb5d23793740c27e07340605f236496cd6ce552043d6"}, @@ -897,29 +1018,29 @@ babel = [ {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, ] black = [ - {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, - {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, - {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, - {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, - {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, - {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, - {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, - {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, - {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, - {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, - {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, - {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, - {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, - {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, - {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, - {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, - {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, - {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, - {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, - {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, - {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, - {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, - {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, + {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, + {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, + {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, + {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"}, + {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"}, + {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"}, + {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"}, + {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"}, + {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"}, + {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"}, + {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"}, + {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"}, + {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"}, + {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"}, + {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"}, + {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"}, + {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"}, + {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"}, + {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"}, + {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"}, + {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"}, + {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"}, + {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"}, ] certifi = [ {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, @@ -930,8 +1051,8 @@ cfgv = [ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, - {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, + {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, + {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, ] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, @@ -941,6 +1062,10 @@ colorama = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] +commitizen = [ + {file = "commitizen-2.28.0-py3-none-any.whl", hash = "sha256:d222f68da12a3ebcaf85c270f19eec7caacbe904349f1823deca6b5e7c2fc0f5"}, + {file = "commitizen-2.28.0.tar.gz", hash = "sha256:8510b67e4c45131ef75114aeca5fe30b4f973b2b943457cf1667177af296192e"}, +] commonmark = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, @@ -988,6 +1113,10 @@ coverage = [ {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, ] +decli = [ + {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, + {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, +] distlib = [ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, @@ -1008,6 +1137,10 @@ flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] +flake8-docstrings = [ + {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, + {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, +] identify = [ {file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"}, {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"}, @@ -1017,8 +1150,8 @@ idna = [ {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] imagesize = [ - {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, - {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] importlib-metadata = [ {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, @@ -1161,6 +1294,10 @@ pre-commit = [ {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"}, {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"}, ] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.30-py3-none-any.whl", hash = "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"}, + {file = "prompt_toolkit-3.0.30.tar.gz", hash = "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0"}, +] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -1184,6 +1321,10 @@ pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] +pydocstyle = [ + {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, + {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, +] pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, @@ -1247,6 +1388,10 @@ pyyaml = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] +questionary = [ + {file = "questionary-1.10.0-py3-none-any.whl", hash = "sha256:fecfcc8cca110fda9d561cb83f1e97ecbb93c613ff857f655818839dac74ce90"}, + {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, +] readthedocs-sphinx-ext = [ {file = "readthedocs-sphinx-ext-2.1.8.tar.gz", hash = "sha256:a57e3713daf77bf91d1ba19e4b9888a47c0abfeb63ecf02e3ac77fcfd99bfe69"}, {file = "readthedocs_sphinx_ext-2.1.8-py2.py3-none-any.whl", hash = "sha256:5ab5875993191e5e526ca196a1082b73116b0cefd79073ab25367ba0458fffe9"}, @@ -1256,8 +1401,8 @@ recommonmark = [ {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, ] requests = [ - {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"}, - {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"}, + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] rsa = [ {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, @@ -1307,6 +1452,9 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] +termcolor = [ + {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -1315,9 +1463,13 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +tomlkit = [ + {file = "tomlkit-0.11.0-py3-none-any.whl", hash = "sha256:0f4050db66fd445b885778900ce4dd9aea8c90c4721141fde0d6ade893820ef1"}, + {file = "tomlkit-0.11.0.tar.gz", hash = "sha256:71ceb10c0eefd8b8f11fe34e8a51ad07812cb1dc3de23247425fbc9ddc47b9dd"}, +] tox = [ - {file = "tox-3.25.0-py2.py3-none-any.whl", hash = "sha256:0805727eb4d6b049de304977dfc9ce315a1938e6619c3ab9f38682bb04662a5a"}, - {file = "tox-3.25.0.tar.gz", hash = "sha256:37888f3092aa4e9f835fc8cc6dadbaaa0782651c41ef359e3a5743fcb0308160"}, + {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"}, + {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"}, ] typed-ast = [ {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, @@ -1346,8 +1498,8 @@ typed-ast = [ {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] typing-extensions = [ - {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, - {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, ] unidecode = [ {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, @@ -1358,8 +1510,12 @@ urllib3 = [ {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, ] virtualenv = [ - {file = "virtualenv-20.15.0-py2.py3-none-any.whl", hash = "sha256:804cce4de5b8a322f099897e308eecc8f6e2951f1a8e7e2b3598dff865f01336"}, - {file = "virtualenv-20.15.0.tar.gz", hash = "sha256:4c44b1d77ca81f8368e2d7414f9b20c428ad16b343ac6d226206c5b84e2b4fcc"}, + {file = "virtualenv-20.15.1-py2.py3-none-any.whl", hash = "sha256:b30aefac647e86af6d82bfc944c556f8f1a9c90427b2fb4e3bfbf338cb82becf"}, + {file = "virtualenv-20.15.1.tar.gz", hash = "sha256:288171134a2ff3bfb1a2f54f119e77cd1b81c29fc1265a2356f3e8d14c7d58c4"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] wrapt = [ {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, diff --git a/pyproject.toml b/pyproject.toml index ae6fc1b..0e54eaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,8 @@ pre-commit = "^2.19.0" isort = "^5.10.1" black = "^22.3.0" flake8 = "^3.5.0" +flake8-docstrings = "^1.6.0" +commitizen = "^2.28.0" [tool.poetry.extras] docs = [ diff --git a/tox.ini b/tox.ini index 0458821..3fbdd66 100644 --- a/tox.ini +++ b/tox.ini @@ -42,3 +42,5 @@ commands = [flake8] max-line-length = 99 +docstring-convention = all +ignore = D203, D213, W503 From 6f839cbc03933a6cac3355993e83bbe9cfeddc23 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 19:59:53 +0000 Subject: [PATCH 07/19] docs: added docstrings to exceptions --- src/keycloak/exceptions.py | 42 +++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/keycloak/exceptions.py b/src/keycloak/exceptions.py index 925c937..8eb69bf 100644 --- a/src/keycloak/exceptions.py +++ b/src/keycloak/exceptions.py @@ -21,12 +21,22 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Keycloak custom exeptions module.""" + import requests class KeycloakError(Exception): - def __init__(self, error_message="", response_code=None, response_body=None): + """Base class for custom Keycloak errors. + + :param error_message: The error message + :type error_message: str + :param response_code: The response status code + :type response_code: int + """ + def __init__(self, error_message="", response_code=None, response_body=None): + """Init method.""" Exception.__init__(self, error_message) self.response_code = response_code @@ -34,6 +44,7 @@ class KeycloakError(Exception): self.error_message = error_message def __str__(self): + """Str method.""" if self.response_code is not None: return "{0}: {1}".format(self.response_code, self.error_message) else: @@ -41,62 +52,91 @@ class KeycloakError(Exception): class KeycloakAuthenticationError(KeycloakError): + """Keycloak authentication error exception.""" + pass class KeycloakConnectionError(KeycloakError): + """Keycloak connection error exception.""" + pass class KeycloakOperationError(KeycloakError): + """Keycloak operation error exception.""" + pass class KeycloakDeprecationError(KeycloakError): + """Keycloak deprecation error exception.""" + pass class KeycloakGetError(KeycloakOperationError): + """Keycloak request get error exception.""" + pass class KeycloakPostError(KeycloakOperationError): + """Keycloak request post error exception.""" + pass class KeycloakPutError(KeycloakOperationError): + """Keycloak request put error exception.""" + pass class KeycloakDeleteError(KeycloakOperationError): + """Keycloak request delete error exception.""" + pass class KeycloakSecretNotFound(KeycloakOperationError): + """Keycloak secret not found exception.""" + pass class KeycloakRPTNotFound(KeycloakOperationError): + """Keycloak RPT not found exception.""" + pass class KeycloakAuthorizationConfigError(KeycloakOperationError): + """Keycloak authorization config exception.""" + pass class KeycloakInvalidTokenError(KeycloakOperationError): + """Keycloak invalid token exception.""" + pass class KeycloakPermissionFormatError(KeycloakOperationError): + """Keycloak permission format exception.""" + pass class PermissionDefinitionError(Exception): + """Keycloak permission definition exception.""" + pass def raise_error_from_response(response, error, expected_codes=None, skip_exists=False): + """Raise an exception for the response.""" if expected_codes is None: expected_codes = [200, 201, 204] From bead0aff2bf5fdd7c966a0cc28fd1910c2b3e9ff Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 20:00:46 +0000 Subject: [PATCH 08/19] fix: raise correct exceptions --- src/keycloak/keycloak_admin.py | 2 +- src/keycloak/keycloak_openid.py | 27 ++++++++------------------- tests/test_keycloak_admin.py | 2 +- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 44e9c3b..b2c1de4 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2736,7 +2736,7 @@ class KeycloakAdmin: else: try: self.token = self.keycloak_openid.refresh_token(refresh_token) - except KeycloakGetError as e: + except KeycloakPostError as e: list_errors = [ b"Refresh token expired", b"Token is not active", diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 3e045bc..ede9a3c 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -229,7 +229,7 @@ class KeycloakOpenID: payload = self._add_secret_key(payload) data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def refresh_token(self, refresh_token, grant_type=["refresh_token"]): """ @@ -252,7 +252,7 @@ class KeycloakOpenID: } payload = self._add_secret_key(payload) data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def exchange_token(self, token: str, client_id: str, audience: str, subject: str) -> dict: """ @@ -276,7 +276,7 @@ class KeycloakOpenID: } payload = self._add_secret_key(payload) data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def userinfo(self, token): """ @@ -288,12 +288,9 @@ class KeycloakOpenID: :param token: :return: """ - 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)) - return raise_error_from_response(data_raw, KeycloakGetError) def logout(self, refresh_token): @@ -304,11 +301,9 @@ 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) - - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def certs(self): """ @@ -367,7 +362,6 @@ class KeycloakOpenID: :return: """ params_path = {"realm-name": self.realm_name} - payload = {"client_id": self.client_id, "token": token} if token_type_hint == "requesting_party_token": @@ -380,8 +374,7 @@ class KeycloakOpenID: payload = self._add_secret_key(payload) data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), data=payload) - - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def decode_token(self, token, key, algorithms=["RS256"], **kwargs): """ @@ -399,7 +392,6 @@ class KeycloakOpenID: :param algorithms: :return: """ - return jwt.decode(token, key, algorithms=algorithms, audience=self.client_id, **kwargs) def load_authorization_config(self, path): @@ -409,10 +401,10 @@ class KeycloakOpenID: :param path: settings file (json) :return: """ - authorization_file = open(path, "r") - authorization_json = json.loads(authorization_file.read()) + with open(path, "r") as fp: + authorization_json = json.load(fp) + self.authorization.load_config(authorization_json) - authorization_file.close() def get_policies(self, token, method_token_info="introspect", **kwargs): """ @@ -421,7 +413,6 @@ class KeycloakOpenID: :param token: user token :return: policies list """ - if not self.authorization.policies: raise KeycloakAuthorizationConfigError( "Keycloak settings not found. Load Authorization Keycloak settings." @@ -455,7 +446,6 @@ class KeycloakOpenID: :param kwargs: parameters for decode :return: permissions list """ - if not self.authorization.policies: raise KeycloakAuthorizationConfigError( "Keycloak settings not found. Load Authorization Keycloak settings." @@ -493,7 +483,6 @@ class KeycloakOpenID: :param permissions: list of uma permissions list(resource:scope) requested by the user :return: permissions list """ - permission = build_permission_param(permissions) params_path = {"realm-name": self.realm_name} diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 6f33e03..e62bdda 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1731,7 +1731,7 @@ def test_auto_refresh(admin: KeycloakAdmin, realm: str): verify=admin.verify, ) admin.token["refresh_token"] = "bad" - with pytest.raises(KeycloakGetError) as err: + with pytest.raises(KeycloakPostError) as err: admin.get_realm(realm_name="test-refresh") assert err.match( '400: b\'{"error":"invalid_grant","error_description":"Invalid refresh token"}\'' From 17bfad5ec0b8a6bad887c3964df3d427f8378b47 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 20:01:00 +0000 Subject: [PATCH 09/19] test: added a license test --- tests/test_license.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/test_license.py diff --git a/tests/test_license.py b/tests/test_license.py new file mode 100644 index 0000000..3c6b10e --- /dev/null +++ b/tests/test_license.py @@ -0,0 +1,14 @@ +"""Tests for license.""" +import os + + +def test_license_present(): + """Test that the MIT license is present in the header of each module file.""" + for path, _, files in os.walk("src/keycloak"): + for _file in files: + if _file.endswith(".py"): + with open(os.path.join(path, _file), "r") as fp: + content = fp.read() + assert content.startswith( + "# -*- coding: utf-8 -*-\n#\n# The MIT License (MIT)\n#\n#" + ) From 65a4af15503d483b7b315b7219d0ed36c50e74e7 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 20:02:10 +0000 Subject: [PATCH 10/19] test: added auth_url and token tests, more structure to fixtures --- tests/conftest.py | 85 ++++++++++++++++++++++++++++++++++- tests/test_keycloak_openid.py | 58 ++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ada9820..6023e51 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +"""Fixtures for tests.""" + import os import uuid @@ -7,6 +9,18 @@ from keycloak import KeycloakAdmin, KeycloakOpenID class KeycloakTestEnv(object): + """Wrapper for test Keycloak connection configuration. + + :param host: Hostname + :type host: str + :param port: Port + :type port: str + :param username: Admin username + :type username: str + :param password: Admin password + :type password: str + """ + def __init__( self, host: str = os.environ["KEYCLOAK_HOST"], @@ -14,6 +28,7 @@ class KeycloakTestEnv(object): username: str = os.environ["KEYCLOAK_ADMIN"], password: str = os.environ["KEYCLOAK_ADMIN_PASSWORD"], ): + """Init method.""" self.KEYCLOAK_HOST = host self.KEYCLOAK_PORT = port self.KEYCLOAK_ADMIN = username @@ -21,44 +36,54 @@ class KeycloakTestEnv(object): @property def KEYCLOAK_HOST(self): + """Hostname getter.""" return self._KEYCLOAK_HOST @KEYCLOAK_HOST.setter def KEYCLOAK_HOST(self, value: str): + """Hostname setter.""" self._KEYCLOAK_HOST = value @property def KEYCLOAK_PORT(self): + """Port getter.""" return self._KEYCLOAK_PORT @KEYCLOAK_PORT.setter def KEYCLOAK_PORT(self, value: str): + """Port setter.""" self._KEYCLOAK_PORT = value @property def KEYCLOAK_ADMIN(self): + """Admin username getter.""" return self._KEYCLOAK_ADMIN @KEYCLOAK_ADMIN.setter def KEYCLOAK_ADMIN(self, value: str): + """Admin username setter.""" self._KEYCLOAK_ADMIN = value @property def KEYCLOAK_ADMIN_PASSWORD(self): + """Admin password getter.""" return self._KEYCLOAK_ADMIN_PASSWORD @KEYCLOAK_ADMIN_PASSWORD.setter def KEYCLOAK_ADMIN_PASSWORD(self, value: str): + """Admin password setter.""" self._KEYCLOAK_ADMIN_PASSWORD = value @pytest.fixture def env(): + """Fixture for getting the test environment configuration object.""" return KeycloakTestEnv() @pytest.fixture def admin(env: KeycloakTestEnv): + """Fixture for initialized KeycloakAdmin class.""" return KeycloakAdmin( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", username=env.KEYCLOAK_ADMIN, @@ -68,11 +93,20 @@ def admin(env: KeycloakTestEnv): @pytest.fixture def oid(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin): + """Fixture for initialized KeycloakOpenID class.""" # Set the realm admin.realm_name = realm # Create client client = str(uuid.uuid4()) - client_id = admin.create_client(payload={"name": client, "clientId": client}) + client_id = admin.create_client( + payload={ + "name": client, + "clientId": client, + "enabled": True, + "publicClient": True, + "protocol": "openid-connect", + } + ) # Return OID yield KeycloakOpenID( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", @@ -83,16 +117,61 @@ def oid(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin): admin.delete_client(client_id=client_id) +@pytest.fixture +def oid_with_credentials(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin): + """Fixture for an initialized KeycloakOpenID class and a random user credentials.""" + # Set the realm + admin.realm_name = realm + # Create client + client = str(uuid.uuid4()) + client_id = admin.create_client( + payload={ + "name": client, + "clientId": client, + "enabled": True, + "publicClient": True, + "protocol": "openid-connect", + } + ) + # Create user + username = str(uuid.uuid4()) + password = str(uuid.uuid4()) + user_id = admin.create_user( + payload={ + "username": username, + "email": f"{username}@test.test", + "enabled": True, + "credentials": [{"type": "password", "value": password}], + } + ) + + yield ( + KeycloakOpenID( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + realm_name=realm, + client_id=client, + ), + username, + password, + ) + + # Cleanup + admin.delete_client(client_id=client_id) + admin.delete_user(user_id=user_id) + + @pytest.fixture def realm(admin: KeycloakAdmin) -> str: + """Fixture for a new random realm.""" realm_name = str(uuid.uuid4()) - admin.create_realm(payload={"realm": realm_name}) + admin.create_realm(payload={"realm": realm_name, "enabled": True}) yield realm_name admin.delete_realm(realm_name=realm_name) @pytest.fixture def user(admin: KeycloakAdmin, realm: str) -> str: + """Fixture for a new random user.""" admin.realm_name = realm username = str(uuid.uuid4()) user_id = admin.create_user(payload={"username": username, "email": f"{username}@test.test"}) @@ -102,6 +181,7 @@ def user(admin: KeycloakAdmin, realm: str) -> str: @pytest.fixture def group(admin: KeycloakAdmin, realm: str) -> str: + """Fixture for a new random group.""" admin.realm_name = realm group_name = str(uuid.uuid4()) group_id = admin.create_group(payload={"name": group_name}) @@ -111,6 +191,7 @@ def group(admin: KeycloakAdmin, realm: str) -> str: @pytest.fixture def client(admin: KeycloakAdmin, realm: str) -> str: + """Fixture for a new random client.""" admin.realm_name = realm client = str(uuid.uuid4()) client_id = admin.create_client(payload={"name": client, "clientId": client}) diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 53a76dd..f01b91c 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -1,9 +1,13 @@ +"""Test module for KeycloakOpenID.""" +from unittest import mock + from keycloak.authorization import Authorization from keycloak.connection import ConnectionManager from keycloak.keycloak_openid import KeycloakOpenID def test_keycloak_openid_init(env): + """Test KeycloakOpenId's init method.""" oid = KeycloakOpenID( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", realm_name="master", @@ -18,6 +22,7 @@ def test_keycloak_openid_init(env): def test_well_known(oid: KeycloakOpenID): + """Test the well_known method.""" res = oid.well_known() assert res is not None assert res != dict() @@ -77,3 +82,56 @@ def test_well_known(oid: KeycloakOpenID): "userinfo_signing_alg_values_supported", ]: assert key in res + + +def test_auth_url(env, oid: KeycloakOpenID): + """Test the auth_url method.""" + res = oid.auth_url(redirect_uri="http://test.test/*") + assert ( + res + == f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}/realms/{oid.realm_name}" + + f"/protocol/openid-connect/auth?client_id={oid.client_id}&response_type=code" + + "&redirect_uri=http://test.test/*" + ) + + +def test_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): + """Test the token method.""" + oid, username, password = oid_with_credentials + token = oid.token(username=username, password=password) + assert token == { + "access_token": mock.ANY, + "expires_in": 300, + "not-before-policy": 0, + "refresh_expires_in": 1800, + "refresh_token": mock.ANY, + "scope": "profile email", + "session_state": mock.ANY, + "token_type": "Bearer", + } + + # Test with dummy totp + token = oid.token(username=username, password=password, totp="123456") + assert token == { + "access_token": mock.ANY, + "expires_in": 300, + "not-before-policy": 0, + "refresh_expires_in": 1800, + "refresh_token": mock.ANY, + "scope": "profile email", + "session_state": mock.ANY, + "token_type": "Bearer", + } + + # Test with extra param + token = oid.token(username=username, password=password, extra_param="foo") + assert token == { + "access_token": mock.ANY, + "expires_in": 300, + "not-before-policy": 0, + "refresh_expires_in": 1800, + "refresh_token": mock.ANY, + "scope": "profile email", + "session_state": mock.ANY, + "token_type": "Bearer", + } From b10c161ed8f2b786d0bce991748736dd8cf8ad3b Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Mon, 11 Jul 2022 13:23:06 +0000 Subject: [PATCH 11/19] test: added more openid tests --- src/keycloak/keycloak_openid.py | 2 +- test_keycloak_init.sh | 4 +- tests/conftest.py | 55 +++++++++++++++- tests/test_keycloak_openid.py | 107 +++++++++++++++++++++++++++++++- 4 files changed, 161 insertions(+), 7 deletions(-) diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index ede9a3c..fa04e4d 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -346,7 +346,7 @@ class KeycloakOpenID: if data_raw.status_code == 404: return raise_error_from_response(data_raw, KeycloakDeprecationError) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakGetError) # pragma: no cover def introspect(self, token, rpt=None, token_type_hint=None): """ diff --git a/test_keycloak_init.sh b/test_keycloak_init.sh index 82afabb..e9c6823 100755 --- a/test_keycloak_init.sh +++ b/test_keycloak_init.sh @@ -10,7 +10,7 @@ function keycloak_stop() { function keycloak_start() { echo "Starting keycloak docker container" - docker run -d --name unittest_keycloak -e KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN}" -e KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD}" -p "${KEYCLOAK_PORT}:8080" "${KEYCLOAK_DOCKER_IMAGE}" start-dev + docker run -d --name unittest_keycloak -e KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN}" -e KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD}" -e KC_FEATURES="token-exchange" -p "${KEYCLOAK_PORT}:8080" "${KEYCLOAK_DOCKER_IMAGE}" start-dev SECONDS=0 until curl --silent --output /dev/null localhost:$KEYCLOAK_PORT; do sleep 5; @@ -28,7 +28,7 @@ keycloak_stop # In case it did not shut down correctly last time. keycloak_start eval ${CMD_ARGS} -docker logs unittest_keycloak > keycloak_test_logs.txt RETURN_VALUE=$? +docker logs unittest_keycloak > keycloak_test_logs.txt exit ${RETURN_VALUE} diff --git a/tests/conftest.py b/tests/conftest.py index 6023e51..47c9854 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -124,13 +124,65 @@ def oid_with_credentials(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin) admin.realm_name = realm # Create client client = str(uuid.uuid4()) + secret = str(uuid.uuid4()) client_id = admin.create_client( payload={ "name": client, "clientId": client, "enabled": True, - "publicClient": True, + "publicClient": False, + "protocol": "openid-connect", + "secret": secret, + "clientAuthenticatorType": "client-secret", + } + ) + # Create user + username = str(uuid.uuid4()) + password = str(uuid.uuid4()) + user_id = admin.create_user( + payload={ + "username": username, + "email": f"{username}@test.test", + "enabled": True, + "credentials": [{"type": "password", "value": password}], + } + ) + + yield ( + KeycloakOpenID( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + realm_name=realm, + client_id=client, + client_secret_key=secret, + ), + username, + password, + ) + + # Cleanup + admin.delete_client(client_id=client_id) + admin.delete_user(user_id=user_id) + + +@pytest.fixture +def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin): + """Fixture for an initialized KeycloakOpenID class and a random user credentials.""" + # Set the realm + admin.realm_name = realm + # Create client + client = str(uuid.uuid4()) + secret = str(uuid.uuid4()) + client_id = admin.create_client( + payload={ + "name": client, + "clientId": client, + "enabled": True, + "publicClient": False, "protocol": "openid-connect", + "secret": secret, + "clientAuthenticatorType": "client-secret", + "authorizationServicesEnabled": True, + "serviceAccountsEnabled": True, } ) # Create user @@ -150,6 +202,7 @@ def oid_with_credentials(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin) server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", realm_name=realm, client_id=client, + client_secret_key=secret, ), username, password, diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index f01b91c..9ed2b88 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -1,8 +1,12 @@ """Test module for KeycloakOpenID.""" from unittest import mock +import pytest + from keycloak.authorization import Authorization from keycloak.connection import ConnectionManager +from keycloak.exceptions import KeycloakDeprecationError, KeycloakRPTNotFound +from keycloak.keycloak_admin import KeycloakAdmin from keycloak.keycloak_openid import KeycloakOpenID @@ -105,7 +109,7 @@ def test_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): "not-before-policy": 0, "refresh_expires_in": 1800, "refresh_token": mock.ANY, - "scope": "profile email", + "scope": mock.ANY, "session_state": mock.ANY, "token_type": "Bearer", } @@ -118,7 +122,7 @@ def test_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): "not-before-policy": 0, "refresh_expires_in": 1800, "refresh_token": mock.ANY, - "scope": "profile email", + "scope": mock.ANY, "session_state": mock.ANY, "token_type": "Bearer", } @@ -131,7 +135,104 @@ def test_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): "not-before-policy": 0, "refresh_expires_in": 1800, "refresh_token": mock.ANY, - "scope": "profile email", + "scope": mock.ANY, "session_state": mock.ANY, "token_type": "Bearer", } + + +def test_exchange_token( + oid_with_credentials: tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin +): + """Test the exchange token method.""" + # Verify existing user + oid, username, password = oid_with_credentials + + # 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_name="realm-management"), + roles=[ + admin.get_client_role( + client_id=admin.get_client_id(client_name="realm-management"), + role_name="impersonation", + ) + ], + ) + + token = oid.token(username=username, password=password) + assert oid.userinfo(token=token["access_token"]) == { + "email": f"{username}@test.test", + "email_verified": False, + "preferred_username": username, + "sub": mock.ANY, + } + + # Exchange token with the new user + new_token = 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"]) == { + "email": f"{username}@test.test", + "email_verified": False, + "preferred_username": username, + "sub": mock.ANY, + } + assert token != new_token + + +def test_certs(oid: KeycloakOpenID): + """Test certificates.""" + assert len(oid.certs()["keys"]) == 2 + + +def test_public_key(oid: KeycloakOpenID): + """Test public key.""" + assert oid.public_key() is not None + + +def test_entitlement( + oid_with_credentials_authz: tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin +): + """Test entitlement.""" + 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"] + + with pytest.raises(KeycloakDeprecationError): + oid.entitlement(token=token["access_token"], resource_server_id=resource_server_id) + + +def test_introspect(oid_with_credentials: tuple[KeycloakOpenID, str, str]): + """Test introspect.""" + oid, username, password = oid_with_credentials + token = oid.token(username=username, password=password) + + assert oid.introspect(token=token["access_token"])["active"] + assert oid.introspect( + token=token["access_token"], rpt="some", token_type_hint="requesting_party_token" + ) == {"active": False} + + with pytest.raises(KeycloakRPTNotFound): + oid.introspect(token=token["access_token"], token_type_hint="requesting_party_token") + + +def test_decode_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): + """Test decode token.""" + oid, username, password = oid_with_credentials + token = oid.token(username=username, password=password) + + 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"] + == username + ) From 5e6c775735d9874a3b9b0df89bd30faeb8bc7e3e Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 12 Jul 2022 08:00:35 +0000 Subject: [PATCH 12/19] style: fixed docstrings everywhere --- poetry.lock | 44 +- src/keycloak/__init__.py | 2 + src/keycloak/authorization/__init__.py | 10 +- src/keycloak/authorization/permission.py | 14 +- src/keycloak/authorization/policy.py | 20 +- src/keycloak/authorization/role.py | 9 +- src/keycloak/connection.py | 15 +- src/keycloak/keycloak_admin.py | 609 +++++++++-------------- src/keycloak/keycloak_openid.py | 75 +-- src/keycloak/uma_permissions.py | 42 +- src/keycloak/urls_patterns.py | 2 + tests/__init__.py | 1 + tests/test_keycloak_admin.py | 29 ++ tests/test_uma_permissions.py | 28 ++ tests/test_urls_patterns.py | 3 +- 15 files changed, 450 insertions(+), 453 deletions(-) diff --git a/poetry.lock b/poetry.lock index c747f94..06cdcc1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -22,7 +22,7 @@ test = ["coverage", "flake8", "pexpect", "wheel"] [[package]] name = "astroid" -version = "2.11.6" +version = "2.11.7" description = "An abstract syntax tree for Python with inference support." category = "main" optional = true @@ -36,7 +36,7 @@ wrapt = ">=1.11,<2" [[package]] name = "atomicwrites" -version = "1.4.0" +version = "1.4.1" description = "Atomic file writes." category = "dev" optional = false @@ -208,7 +208,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "ecdsa" -version = "0.17.0" +version = "0.18.0" description = "ECDSA cryptographic signature library (pure python)" category = "main" optional = false @@ -460,7 +460,7 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.19.0" +version = "2.20.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -871,7 +871,7 @@ python-versions = ">=3.7" [[package]] name = "tomlkit" -version = "0.11.0" +version = "0.11.1" description = "Style preserving TOML library" category = "dev" optional = false @@ -926,11 +926,11 @@ python-versions = ">=3.5" [[package]] name = "urllib3" -version = "1.26.9" +version = "1.26.10" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] @@ -1001,14 +1001,8 @@ argcomplete = [ {file = "argcomplete-1.12.3-py2.py3-none-any.whl", hash = "sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81"}, {file = "argcomplete-1.12.3.tar.gz", hash = "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445"}, ] -astroid = [ - {file = "astroid-2.11.6-py3-none-any.whl", hash = "sha256:ba33a82a9a9c06a5ceed98180c5aab16e29c285b828d94696bf32d6015ea82a9"}, - {file = "astroid-2.11.6.tar.gz", hash = "sha256:4f933d0bf5e408b03a6feb5d23793740c27e07340605f236496cd6ce552043d6"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] +astroid = [] +atomicwrites = [] attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, @@ -1125,10 +1119,7 @@ docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] -ecdsa = [ - {file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"}, - {file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"}, -] +ecdsa = [] filelock = [ {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, @@ -1290,10 +1281,7 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -pre-commit = [ - {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"}, - {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"}, -] +pre-commit = [] prompt-toolkit = [ {file = "prompt_toolkit-3.0.30-py3-none-any.whl", hash = "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"}, {file = "prompt_toolkit-3.0.30.tar.gz", hash = "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0"}, @@ -1463,10 +1451,7 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -tomlkit = [ - {file = "tomlkit-0.11.0-py3-none-any.whl", hash = "sha256:0f4050db66fd445b885778900ce4dd9aea8c90c4721141fde0d6ade893820ef1"}, - {file = "tomlkit-0.11.0.tar.gz", hash = "sha256:71ceb10c0eefd8b8f11fe34e8a51ad07812cb1dc3de23247425fbc9ddc47b9dd"}, -] +tomlkit = [] tox = [ {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"}, {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"}, @@ -1505,10 +1490,7 @@ unidecode = [ {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, ] -urllib3 = [ - {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, - {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, -] +urllib3 = [] virtualenv = [ {file = "virtualenv-20.15.1-py2.py3-none-any.whl", hash = "sha256:b30aefac647e86af6d82bfc944c556f8f1a9c90427b2fb4e3bfbf338cb82becf"}, {file = "virtualenv-20.15.1.tar.gz", hash = "sha256:288171134a2ff3bfb1a2f54f119e77cd1b81c29fc1265a2356f3e8d14c7d58c4"}, diff --git a/src/keycloak/__init__.py b/src/keycloak/__init__.py index 2c7f70f..694e53d 100644 --- a/src/keycloak/__init__.py +++ b/src/keycloak/__init__.py @@ -21,6 +21,8 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Python-Keycloak library.""" + from ._version import __version__ from .connection import ConnectionManager from .exceptions import ( diff --git a/src/keycloak/authorization/__init__.py b/src/keycloak/authorization/__init__.py index 789656d..fddd551 100644 --- a/src/keycloak/authorization/__init__.py +++ b/src/keycloak/authorization/__init__.py @@ -21,6 +21,8 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Authorization module.""" + import ast import json @@ -30,18 +32,19 @@ from .role import Role class Authorization: - """ - Keycloak Authorization (policies, roles, scopes and resources). + """Keycloak Authorization (policies, roles, scopes and resources). https://keycloak.gitbooks.io/documentation/authorization_services/index.html """ def __init__(self): + """Init method.""" self.policies = {} @property def policies(self): + """Get policies.""" return self._policies @policies.setter @@ -49,8 +52,7 @@ class Authorization: self._policies = value def load_config(self, data): - """ - Load policies, roles and permissions (scope/resources). + """Load policies, roles and permissions (scope/resources). :param data: keycloak authorization data (dict) :returns: None diff --git a/src/keycloak/authorization/permission.py b/src/keycloak/authorization/permission.py index a200afe..a444f83 100644 --- a/src/keycloak/authorization/permission.py +++ b/src/keycloak/authorization/permission.py @@ -21,9 +21,12 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Keycloak authorization Permission module.""" + class Permission: - """ + """Base permission class. + Consider this simple and very common permission: A permission associates the object being protected with the policies that must be evaluated to @@ -45,6 +48,7 @@ class Permission: """ def __init__(self, name, type, logic, decision_strategy): + """Init method.""" self._name = name self._type = type self._logic = logic @@ -53,13 +57,16 @@ class Permission: self._scopes = [] def __repr__(self): + """Repr method.""" return "" % (self.name, self.type) def __str__(self): + """Str method.""" return "Permission: %s (%s)" % (self.name, self.type) @property def name(self): + """Get name.""" return self._name @name.setter @@ -68,6 +75,7 @@ class Permission: @property def type(self): + """Get type.""" return self._type @type.setter @@ -76,6 +84,7 @@ class Permission: @property def logic(self): + """Get logic.""" return self._logic @logic.setter @@ -84,6 +93,7 @@ class Permission: @property def decision_strategy(self): + """Get decision strategy.""" return self._decision_strategy @decision_strategy.setter @@ -92,6 +102,7 @@ class Permission: @property def resources(self): + """Get resources.""" return self._resources @resources.setter @@ -100,6 +111,7 @@ class Permission: @property def scopes(self): + """Get scopes.""" return self._scopes @scopes.setter diff --git a/src/keycloak/authorization/policy.py b/src/keycloak/authorization/policy.py index 4014b7a..6b558d8 100644 --- a/src/keycloak/authorization/policy.py +++ b/src/keycloak/authorization/policy.py @@ -21,11 +21,14 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Keycloak authorization Policy module.""" + from ..exceptions import KeycloakAuthorizationConfigError class Policy: - """ + """Base policy class. + A policy defines the conditions that must be satisfied to grant access to an object. Unlike permissions, you do not specify the object being protected but rather the conditions that must be satisfied for access to a given object (for example, resource, scope, or both). @@ -39,6 +42,7 @@ class Policy: """ def __init__(self, name, type, logic, decision_strategy): + """Init method.""" self._name = name self._type = type self._logic = logic @@ -47,13 +51,16 @@ class Policy: self._permissions = [] def __repr__(self): + """Repr method.""" return "" % (self.name, self.type) def __str__(self): + """Str method.""" return "Policy: %s (%s)" % (self.name, self.type) @property def name(self): + """Get name.""" return self._name @name.setter @@ -62,6 +69,7 @@ class Policy: @property def type(self): + """Get type.""" return self._type @type.setter @@ -70,6 +78,7 @@ class Policy: @property def logic(self): + """Get logic.""" return self._logic @logic.setter @@ -78,6 +87,7 @@ class Policy: @property def decision_strategy(self): + """Get decision strategy.""" return self._decision_strategy @decision_strategy.setter @@ -86,15 +96,16 @@ class Policy: @property def roles(self): + """Get roles.""" return self._roles @property def permissions(self): + """Get permissions.""" return self._permissions def add_role(self, role): - """ - Add keycloak role in policy. + """Add keycloak role in policy. :param role: keycloak role. :return: @@ -106,8 +117,7 @@ class Policy: self._roles.append(role) def add_permission(self, permission): - """ - Add keycloak permission in policy. + """Add keycloak permission in policy. :param permission: keycloak permission. :return: diff --git a/src/keycloak/authorization/role.py b/src/keycloak/authorization/role.py index 3ff06dd..05da243 100644 --- a/src/keycloak/authorization/role.py +++ b/src/keycloak/authorization/role.py @@ -21,25 +21,30 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""The authorization Role module.""" + class Role: - """ + """Authorization Role base class. + Roles identify a type or category of user. Admin, user, manager, and employee are all typical roles that may exist in an organization. https://keycloak.gitbooks.io/documentation/server_admin/topics/roles.html - """ def __init__(self, name, required=False): + """Init method.""" self.name = name self.required = required @property def get_name(self): + """Get name.""" return self.name def __eq__(self, other): + """Eq method.""" if isinstance(other, str): return self.name == other return NotImplemented diff --git a/src/keycloak/connection.py b/src/keycloak/connection.py index 0757377..361d95d 100644 --- a/src/keycloak/connection.py +++ b/src/keycloak/connection.py @@ -21,6 +21,8 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Connection manager module.""" + try: from urllib.parse import urljoin except ImportError: @@ -33,8 +35,7 @@ from .exceptions import KeycloakConnectionError class ConnectionManager(object): - """ - Represents a simple server connection. + """Represents a simple server connection. :param base_url: (str) The server URL. :param headers: (dict) The header parameters of the requests to the server. @@ -44,6 +45,7 @@ class ConnectionManager(object): """ def __init__(self, base_url, headers={}, timeout=60, verify=True, proxies=None): + """Init method.""" self._base_url = base_url self._headers = headers self._timeout = timeout @@ -66,6 +68,7 @@ class ConnectionManager(object): self._s.proxies.update(proxies) def __del__(self): + """Del method.""" self._s.close() @property @@ -75,7 +78,6 @@ class ConnectionManager(object): @base_url.setter def base_url(self, value): - """ """ self._base_url = value @property @@ -85,7 +87,6 @@ class ConnectionManager(object): @timeout.setter def timeout(self, value): - """ """ self._timeout = value @property @@ -95,7 +96,6 @@ class ConnectionManager(object): @verify.setter def verify(self, value): - """ """ self._verify = value @property @@ -105,12 +105,10 @@ class ConnectionManager(object): @headers.setter def headers(self, value): - """ """ self._headers = value def param_headers(self, key): - """ - Return a specific header parameter. + """Return a specific header parameter. :param key: (str) Header parameters key. :returns: If the header parameters exist, return its value. @@ -151,7 +149,6 @@ class ConnectionManager(object): :returns: Response the request. :raises: HttpError Can't connect to server. """ - try: return self._s.get( urljoin(self.base_url, path), diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index b2c1de4..0825730 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -24,6 +24,8 @@ # Unless otherwise stated in the comments, "id", in e.g. user_id, refers to the # internal Keycloak server ID, usually a uuid string +"""The keycloak admin module.""" + import json from builtins import isinstance from typing import Iterable @@ -41,8 +43,7 @@ from .keycloak_openid import KeycloakOpenID class KeycloakAdmin: - """ - Keycloak Admin client. + """Keycloak Admin client. :param server_url: Keycloak server url :param username: admin username @@ -90,6 +91,7 @@ class KeycloakAdmin: auto_refresh_token=None, timeout=60, ): + """Init method.""" self.server_url = server_url self.username = username self.password = password @@ -108,6 +110,7 @@ class KeycloakAdmin: @property def server_url(self): + """Get server url.""" return self._server_url @server_url.setter @@ -116,6 +119,7 @@ class KeycloakAdmin: @property def realm_name(self): + """Get realm name.""" return self._realm_name @realm_name.setter @@ -124,6 +128,7 @@ class KeycloakAdmin: @property def connection(self): + """Get connection.""" return self._connection @connection.setter @@ -132,6 +137,7 @@ class KeycloakAdmin: @property def client_id(self): + """Get client id.""" return self._client_id @client_id.setter @@ -140,6 +146,7 @@ class KeycloakAdmin: @property def client_secret_key(self): + """Get client secret key.""" return self._client_secret_key @client_secret_key.setter @@ -148,6 +155,7 @@ class KeycloakAdmin: @property def verify(self): + """Get verify.""" return self._verify @verify.setter @@ -156,6 +164,7 @@ class KeycloakAdmin: @property def username(self): + """Get username.""" return self._username @username.setter @@ -164,6 +173,7 @@ class KeycloakAdmin: @property def password(self): + """Get password.""" return self._password @password.setter @@ -172,6 +182,7 @@ class KeycloakAdmin: @property def totp(self): + """Get totp.""" return self._totp @totp.setter @@ -180,6 +191,7 @@ class KeycloakAdmin: @property def token(self): + """Get token.""" return self._token @token.setter @@ -188,10 +200,12 @@ class KeycloakAdmin: @property def auto_refresh_token(self): + """Get auto refresh token.""" return self._auto_refresh_token @property def user_realm_name(self): + """Get user realm name.""" return self._user_realm_name @user_realm_name.setter @@ -200,6 +214,7 @@ class KeycloakAdmin: @property def custom_headers(self): + """Get custom headers.""" return self._custom_headers @custom_headers.setter @@ -223,7 +238,9 @@ class KeycloakAdmin: self._auto_refresh_token = value def __fetch_all(self, url, query=None): - """Wrapper function to paginate GET requests + """Paginate over get requests. + + Wrapper function to paginate GET requests. :param url: The url on which the query is executed :param query: Existing query parameters (optional) @@ -258,8 +275,9 @@ class KeycloakAdmin: return raise_error_from_response(self.raw_get(url, **query), KeycloakGetError) def import_realm(self, payload): - """ - Import a new realm from a RealmRepresentation. Realm name must be unique. + """Import a new realm from a RealmRepresentation. + + Realm name must be unique. RealmRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmrepresentation @@ -268,13 +286,11 @@ class KeycloakAdmin: :return: RealmRepresentation """ - data_raw = self.raw_post(urls_patterns.URL_ADMIN_REALMS, data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def export_realm(self, export_clients=False, export_groups_and_role=False): - """ - Export the realm configurations in the json format + """Export the realm configurations in the json format. RealmRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_partialexport @@ -295,8 +311,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError) def get_realms(self): - """ - Lists all realms in Keycloak deployment + """List all realms in Keycloak deployment. :return: realms list """ @@ -304,8 +319,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_realm(self, realm_name): - """ - Get a specific realm. + """Get a specific realm. RealmRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation @@ -318,8 +332,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) def create_realm(self, payload, skip_exists=False): - """ - Create a realm + """Create a realm. RealmRepresentation: https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmrepresentation @@ -328,15 +341,15 @@ class KeycloakAdmin: :param skip_exists: Skip if Realm already exist. :return: Keycloak server response (RealmRepresentation) """ - data_raw = self.raw_post(urls_patterns.URL_ADMIN_REALMS, data=json.dumps(payload)) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def update_realm(self, realm_name, payload): - """ - Update a realm. This wil only update top level attributes and will ignore any user, + """Update a realm. + + This wil only update top level attributes and will ignore any user, role, or client information in the payload. RealmRepresentation: @@ -346,7 +359,6 @@ class KeycloakAdmin: :param payload: RealmRepresentation :return: Http response """ - params_path = {"realm-name": realm_name} data_raw = self.raw_put( urls_patterns.URL_ADMIN_REALM.format(**params_path), data=json.dumps(payload) @@ -354,19 +366,18 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_realm(self, realm_name): - """ - Delete a realm + """Delete a realm. :param realm_name: Realm name (not the realm id) :return: Http response """ - params_path = {"realm-name": realm_name} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_REALM.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_users(self, query=None): - """ + """Get all users. + Return a list of users, filtered according to query parameters UserRepresentation @@ -385,8 +396,7 @@ class KeycloakAdmin: return self.__fetch_all(url, query) def create_idp(self, payload): - """ - Create an ID Provider, + """Create an ID Provider. IdentityProviderRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_identityproviderrepresentation @@ -400,8 +410,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def update_idp(self, idp_alias, payload): - """ - Update an ID Provider + """Update an ID Provider. IdentityProviderRepresentation https://www.keycloak.org/docs-api/15.0/rest-api/index.html#_identity_providers_resource @@ -416,8 +425,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def add_mapper_to_idp(self, idp_alias, payload): - """ - Create an ID Provider, + """Create an ID Provider. IdentityProviderRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_identityprovidermapperrepresentation @@ -432,8 +440,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def update_mapper_in_idp(self, idp_alias, mapper_id, payload): - """ - Update an IdP mapper + """Update an IdP mapper. IdentityProviderMapperRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_update @@ -457,7 +464,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_idp_mappers(self, idp_alias): - """ + """Get IDP mappers. + Returns a list of ID Providers mappers IdentityProviderMapperRepresentation @@ -471,7 +479,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_idps(self): - """ + """Get IDPs. + Returns a list of ID Providers, IdentityProviderRepresentation @@ -484,8 +493,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def delete_idp(self, idp_alias): - """ - Deletes ID Provider, + """Delete an ID Provider. :param: idp_alias: idp alias name """ @@ -494,8 +502,9 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def create_user(self, payload, exist_ok=False): - """ - Create a new user. Username must be unique + """Create a new user. + + Username must be unique UserRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation @@ -522,8 +531,7 @@ class KeycloakAdmin: return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def users_count(self, query=None): - """ - User counter + """Count users. https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_users_resource @@ -537,8 +545,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_user_id(self, username): - """ - Get internal keycloak user id from username + """Get internal keycloak user id from username. + This is required for further actions against this user. UserRepresentation @@ -553,8 +561,7 @@ class KeycloakAdmin: return next((user["id"] for user in users if user["username"] == lower_user_name), None) def get_user(self, user_id): - """ - Get representation of the user + """Get representation of the user. :param user_id: User id @@ -568,7 +575,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_user_groups(self, user_id): - """ + """Get user groups. + Returns a list of groups of which the user is a member :param user_id: User id @@ -580,8 +588,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def update_user(self, user_id, payload): - """ - Update the user + """Update the user. :param user_id: User id :param payload: UserRepresentation @@ -595,8 +602,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_user(self, user_id): - """ - Delete the user + """Delete the user. :param user_id: User id @@ -607,8 +613,9 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def set_user_password(self, user_id, password, temporary=True): - """ - Set up a password for the user. If temporary is True, the user will have to reset + """Set up a password for the user. + + If temporary is True, the user will have to reset the temporary password next time they log in. https://www.keycloak.org/docs-api/18.0/rest-api/#_users_resource @@ -628,7 +635,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_credentials(self, user_id): - """ + """Get user credentials. + Returns a list of credential belonging to the user. CredentialRepresentation @@ -642,8 +650,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def delete_credential(self, user_id, credential_id): - """ - Delete credential of the user. + """Delete credential of the user. CredentialRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_credentialrepresentation @@ -661,8 +668,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError) def user_logout(self, user_id): - """ - Logs out user. + """Log out the user. https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_logout @@ -676,8 +682,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def user_consents(self, user_id): - """ - Get consents granted by the user + """Get consents granted by the user. UserConsentRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userconsentrepresentation @@ -690,7 +695,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_user_social_logins(self, user_id): - """ + """Get user social logins. + Returns a list of federated identities/social logins of which the user has been associated with :param user_id: User id @@ -703,9 +709,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def add_user_social_login(self, user_id, provider_id, provider_userid, provider_username): + """Add a federated identity / social login provider to the user. - """ - Add a federated identity / social login provider to the user :param user_id: User id :param provider_id: Social login provider id :param provider_userid: userid specified by the provider @@ -725,9 +730,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201, 204]) def delete_user_social_login(self, user_id, provider_id): + """Delete a federated identity / social login provider from the user. - """ - Delete a federated identity / social login provider from the user :param user_id: User id :param provider_id: Social login provider id :return: @@ -741,9 +745,9 @@ class KeycloakAdmin: def send_update_account( self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None ): - """ - Send an update account email to the user. An email contains a - link the user can click to perform a set of required actions. + """Send an update account email to the user. + + An email contains a link the user can click to perform a set of required actions. :param user_id: User id :param payload: A list of actions for the user to complete @@ -763,9 +767,9 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError) def send_verify_email(self, user_id, client_id=None, redirect_uri=None): - """ - Send a update account email to the user An email contains a - link the user can click to perform a set of required actions. + """Send a update account email to the user. + + An email contains a link the user can click to perform a set of required actions. :param user_id: User id :param client_id: Client id (optional) @@ -783,8 +787,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError) def get_sessions(self, user_id): - """ - Get sessions associated with the user + """Get sessions associated with the user. :param user_id: id of user @@ -798,8 +801,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_server_info(self): - """ - Get themes, social providers, auth providers, and event listeners available on this server + """Get themes, social providers, auth providers, and event listeners available on this server. ServerInfoRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_serverinforepresentation @@ -810,7 +812,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_groups(self, query=None): - """ + """Get groups. + Returns a list of groups belonging to the realm GroupRepresentation @@ -828,8 +831,9 @@ class KeycloakAdmin: return self.__fetch_all(url, query) def get_group(self, group_id): - """ - Get group by id. Returns full group details + """Get group by id. + + Returns full group details GroupRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation @@ -842,7 +846,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_subgroups(self, group, path): - """ + """Get subgroups. + Utility function to iterate through nested group structures GroupRepresentation @@ -853,7 +858,6 @@ class KeycloakAdmin: :return: Keycloak server response (GroupRepresentation) """ - for subgroup in group["subGroups"]: if subgroup["path"] == path: return subgroup @@ -866,8 +870,9 @@ class KeycloakAdmin: return None def get_group_members(self, group_id, query=None): - """ - Get members by group id. Returns group members + """Get members by group id. + + Returns group members GroupRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/#_userrepresentation @@ -887,8 +892,8 @@ class KeycloakAdmin: return self.__fetch_all(url, query) def get_group_by_path(self, path, search_in_subgroups=False): - """ - Get group id based on name or path. + """Get group id based on name or path. + A straight name or path match with a top-level group will return first. Subgroups are traversed, the first to match path (or name with path) is returned. @@ -899,7 +904,6 @@ class KeycloakAdmin: :param search_in_subgroups: True if want search in the subgroups :return: Keycloak server response (GroupRepresentation) """ - groups = self.get_groups() # TODO: Review this code is necessary @@ -916,8 +920,7 @@ class KeycloakAdmin: return None def create_group(self, payload, parent=None, skip_exists=False): - """ - Creates a group in the Realm + """Create a group in the Realm. :param payload: GroupRepresentation :param parent: parent group's id. Required to create a sub-group. @@ -928,7 +931,6 @@ class KeycloakAdmin: :return: Group id for newly created group or None for an existing group """ - if parent is None: params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( @@ -950,8 +952,7 @@ class KeycloakAdmin: return def update_group(self, group_id, payload): - """ - Update group, ignores subgroups. + """Update group, ignores subgroups. :param group_id: id of group :param payload: GroupRepresentation with updated information. @@ -961,7 +962,6 @@ class KeycloakAdmin: :return: Http response """ - params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_put( urls_patterns.URL_ADMIN_GROUP.format(**params_path), data=json.dumps(payload) @@ -969,14 +969,14 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def group_set_permissions(self, group_id, enabled=True): - """ - Enable/Disable permissions for a group. Cannot delete group if disabled + """Enable/Disable permissions for a group. + + Cannot delete group if disabled :param group_id: id of group :param enabled: boolean :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_put( urls_patterns.URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), @@ -985,14 +985,12 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError) def group_user_add(self, user_id, group_id): - """ - Add user to group (user_id and group_id) + """Add user to group (user_id and group_id). :param user_id: id of user :param group_id: id of group to add to :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_put( urls_patterns.URL_ADMIN_USER_GROUP.format(**params_path), data=None @@ -1000,32 +998,29 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def group_user_remove(self, user_id, group_id): - """ - Remove user from group (user_id and group_id) + """Remove user from group (user_id and group_id). :param user_id: id of user :param group_id: id of group to remove from :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_USER_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def delete_group(self, group_id): - """ - Deletes a group in the Realm + """Delete a group in the Realm. :param group_id: id of group to delete :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_clients(self): - """ + """Get clients. + Returns a list of clients belonging to the realm ClientRepresentation @@ -1033,14 +1028,12 @@ class KeycloakAdmin: :return: Keycloak server response (ClientRepresentation) """ - params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client(self, client_id): - """ - Get representation of the client + """Get representation of the client. ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -1048,21 +1041,19 @@ class KeycloakAdmin: :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_id(self, client_name): - """ - Get internal keycloak client id from client-id. + """Get internal keycloak client id from client-id. + This is required for further actions against this client. :param client_name: name in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: client_id (uuid as string) """ - clients = self.get_clients() for client in clients: @@ -1072,14 +1063,12 @@ class KeycloakAdmin: return None def get_client_authz_settings(self, client_id): - """ - Get authorization json from client. + """Get authorization json from client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SETTINGS.format(**params_path) @@ -1087,8 +1076,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def create_client_authz_resource(self, client_id, payload, skip_exists=False): - """ - Create resources of client. + """Create resources of client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -1097,7 +1085,6 @@ class KeycloakAdmin: :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( @@ -1109,14 +1096,12 @@ class KeycloakAdmin: ) def get_client_authz_resources(self, client_id): - """ - Get resources from client. + """Get resources from client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path) @@ -1124,8 +1109,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def create_client_authz_role_based_policy(self, client_id, payload, skip_exists=False): - """ - Create role-based policy of client. + """Create role-based policy of client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -1147,7 +1131,6 @@ class KeycloakAdmin: } """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( @@ -1159,8 +1142,7 @@ class KeycloakAdmin: ) def create_client_authz_resource_based_permission(self, client_id, payload, skip_exists=False): - """ - Create resource-based permission of client. + """Create resource-based permission of client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -1183,7 +1165,6 @@ class KeycloakAdmin: ] """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( @@ -1195,27 +1176,23 @@ class KeycloakAdmin: ) def get_client_authz_scopes(self, client_id): - """ - Get scopes from client. + """Get scopes from client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_permissions(self, client_id): - """ - Get permissions from client. + """Get permissions from client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS.format(**params_path) @@ -1223,14 +1200,12 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_policies(self, client_id): - """ - Get policies from client. + """Get policies from client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path) @@ -1238,14 +1213,12 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_client_service_account_user(self, client_id): - """ - Get service account user from client. + """Get service account user from client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: UserRepresentation """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( urls_patterns.URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER.format(**params_path) @@ -1253,8 +1226,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def create_client(self, payload, skip_exists=False): - """ - Create a client + """Create a client. ClientRepresentation: https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -1263,7 +1235,6 @@ class KeycloakAdmin: :param payload: ClientRepresentation :return: Client ID """ - if skip_exists: client_id = self.get_client_id(client_name=payload["name"]) @@ -1281,8 +1252,7 @@ class KeycloakAdmin: return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def update_client(self, client_id, payload): - """ - Update a client + """Update a client. :param client_id: Client id :param payload: ClientRepresentation @@ -1296,8 +1266,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_client(self, client_id): - """ - Get representation of the client + """Get representation of the client. ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -1305,14 +1274,12 @@ class KeycloakAdmin: :param client_id: keycloak client id (not oauth client-id) :return: Keycloak server response (ClientRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_CLIENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_client_installation_provider(self, client_id, provider_id): - """ - Get content for given installation provider + """Get content for given installation provider. Related documentation: https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource @@ -1323,7 +1290,6 @@ class KeycloakAdmin: :param client_id: Client id :param provider_id: provider id to specify response format """ - params_path = {"realm-name": self.realm_name, "id": client_id, "provider-id": provider_id} data_raw = self.raw_get( urls_patterns.URL_ADMIN_CLIENT_INSTALLATION_PROVIDER.format(**params_path) @@ -1331,22 +1297,20 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) def get_realm_roles(self): - """ - Get all roles for the realm or client + """Get all roles for the realm or client. RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :return: Keycloak server response (RoleRepresentation) """ - params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_realm_role_members(self, role_name, query=None): - """ - Get role members of realm by role name. + """Get role members of realm by role name. + :param role_name: Name of the role. :param query: Additional Query parameters (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_roles_resource) @@ -1359,8 +1323,7 @@ class KeycloakAdmin: ) def get_client_roles(self, client_id): - """ - Get all roles for the client + """Get all roles for the client. :param client_id: id of client (not client-id) @@ -1369,14 +1332,13 @@ class KeycloakAdmin: :return: Keycloak server response (RoleRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_role(self, client_id, role_name): - """ - Get client role id by name + """Get client role id by name. + This is required for further actions with this role. :param client_id: id of client (not client-id) @@ -1392,10 +1354,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_client_role_id(self, client_id, role_name): - """ - Warning: Deprecated + """Get client role id by name. - Get client role id by name This is required for further actions with this role. :param client_id: id of client (not client-id) @@ -1410,8 +1370,7 @@ class KeycloakAdmin: return role.get("id") def create_client_role(self, client_role_id, payload, skip_exists=False): - """ - Create a client role + """Create a client role. RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation @@ -1421,7 +1380,6 @@ class KeycloakAdmin: :param skip_exists: If true then do not raise an error if client role already exists :return: Client role name """ - if skip_exists: try: res = self.get_client_role(client_id=client_role_id, role_name=payload["name"]) @@ -1440,15 +1398,13 @@ class KeycloakAdmin: return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): - """ - Add composite roles to client role + """Add composite roles to client role. :param client_role_id: id of client (not client-id) :param role_name: The name of the role :param roles: roles list or role (use RoleRepresentation) to be updated :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} data_raw = self.raw_post( @@ -1458,8 +1414,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def update_client_role(self, client_role_id, role_name, payload): - """ - Update a client role + """Update a client role. RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation @@ -1475,8 +1430,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_client_role(self, client_role_id, role_name): - """ - Delete a client role + """Delete a client role. RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation @@ -1489,15 +1443,13 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def assign_client_role(self, user_id, client_id, roles): - """ - Assign a client role to a user + """Assign a client role to a user. :param user_id: id of user :param client_id: id of client (not client-id) :param roles: roles list or role (use RoleRepresentation) :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} data_raw = self.raw_post( @@ -1507,8 +1459,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def get_client_role_members(self, client_id, role_name, **query): - """ - Get members by client role . + """Get members by client role. + :param client_id: The client id :param role_name: the name of role to be queried. :param query: Additional query parameters @@ -1521,8 +1473,8 @@ class KeycloakAdmin: ) def get_client_role_groups(self, client_id, role_name, **query): - """ - Get group members by client role . + """Get group members by client role. + :param client_id: The client id :param role_name: the name of role to be queried. :param query: Additional query parameters @@ -1535,14 +1487,12 @@ class KeycloakAdmin: ) def create_realm_role(self, payload, skip_exists=False): - """ - Create a new role for the realm or client + """Create a new role for the realm or client. :param payload: The role (use RoleRepresentation) :param skip_exists: If true then do not raise an error if realm role already exists :return: Realm role name """ - if skip_exists: try: role = self.get_realm_role(role_name=payload["name"]) @@ -1561,8 +1511,8 @@ class KeycloakAdmin: return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def get_realm_role(self, role_name): - """ - Get realm role by role name + """Get realm role by role name. + :param role_name: role's name, not id! RoleRepresentation @@ -1576,13 +1526,12 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def update_realm_role(self, role_name, payload): - """ - Update a role for the realm by name + """Update a role for the realm by name. + :param role_name: The name of the role to be updated :param payload: The role (use RoleRepresentation) :return Keycloak server response """ - params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_put( urls_patterns.URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), @@ -1591,12 +1540,11 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_realm_role(self, role_name): - """ - Delete a role for the realm by name + """Delete a role for the realm by name. + :param payload: The role name {'role-name':'name-of-the-role'} :return Keycloak server response """ - params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_delete( urls_patterns.URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path) @@ -1604,14 +1552,12 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_composite_realm_roles_to_role(self, role_name, roles): - """ - Add composite roles to the role + """Add composite roles to the role. :param role_name: The name of the role :param roles: roles list or role (use RoleRepresentation) to be updated :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_post( @@ -1621,14 +1567,12 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def remove_composite_realm_roles_to_role(self, role_name, roles): - """ - Remove composite roles from the role + """Remove composite roles from the role. :param role_name: The name of the role :param roles: roles list or role (use RoleRepresentation) to be removed :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_delete( @@ -1638,13 +1582,11 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_composite_realm_roles_of_role(self, role_name): - """ - Get composite roles of the role + """Get composite roles of the role. :param role_name: The name of the role :return: Keycloak server response (array RoleRepresentation) """ - params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_get( urls_patterns.URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path) @@ -1652,14 +1594,12 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def assign_realm_roles(self, user_id, roles): - """ - Assign realm roles to a user + """Assign realm roles to a user. :param user_id: id of user :param roles: roles list or role (use RoleRepresentation) :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_post( @@ -1669,14 +1609,12 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def delete_realm_roles_of_user(self, user_id, roles): - """ - Deletes realm roles of a user + """Delete realm roles of a user. :param user_id: id of user :param roles: roles list or role (use RoleRepresentation) :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_delete( @@ -1686,20 +1624,18 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_realm_roles_of_user(self, user_id): - """ - Get all realm roles for a user. + """Get all realm roles for a user. :param user_id: id of user :return: Keycloak server response (array RoleRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_available_realm_roles_of_user(self, user_id): - """ - Get all available (i.e. unassigned) realm roles for a user. + """Get all available (i.e. unassigned) realm roles for a user. + :param user_id: id of user :return: Keycloak server response (array RoleRepresentation) """ @@ -1710,8 +1646,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_composite_realm_roles_of_user(self, user_id): - """ - Get all composite (i.e. implicit) realm roles for a user. + """Get all composite (i.e. implicit) realm roles for a user. + :param user_id: id of user :return: Keycloak server response (array RoleRepresentation) """ @@ -1722,14 +1658,12 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def assign_group_realm_roles(self, group_id, roles): - """ - Assign realm roles to a group + """Assign realm roles to a group. :param group_id: id of groupp :param roles: roles list or role (use GroupRoleRepresentation) :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_post( @@ -1739,14 +1673,12 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def delete_group_realm_roles(self, group_id, roles): - """ - Delete realm roles of a group + """Delete realm roles of a group. :param group_id: id of group :param roles: roles list or role (use GroupRoleRepresentation) :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_delete( @@ -1756,8 +1688,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_group_realm_roles(self, group_id): - """ - Get all realm roles for a group. + """Get all realm roles for a group. :param user_id: id of the group :return: Keycloak server response (array RoleRepresentation) @@ -1767,15 +1698,13 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def assign_group_client_roles(self, group_id, client_id, roles): - """ - Assign client roles to a group + """Assign client roles to a group. :param group_id: id of group :param client_id: id of client (not client-id) :param roles: roles list or role (use GroupRoleRepresentation) :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_post( @@ -1785,28 +1714,24 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def get_group_client_roles(self, group_id, client_id): - """ - Get client roles of a group + """Get client roles of a group. :param group_id: id of group :param client_id: id of client (not client-id) :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def delete_group_client_roles(self, group_id, client_id, roles): - """ - Delete client roles of a group + """Delete client roles of a group. :param group_id: id of group :param client_id: id of client (not client-id) :param roles: roles list or role (use GroupRoleRepresentation) :return: Keycloak server response (array RoleRepresentation) """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_delete( @@ -1816,8 +1741,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_client_roles_of_user(self, user_id, client_id): - """ - Get all client roles for a user. + """Get all client roles for a user. :param user_id: id of user :param client_id: id of client (not client-id) @@ -1828,8 +1752,7 @@ class KeycloakAdmin: ) def get_available_client_roles_of_user(self, user_id, client_id): - """ - Get available client role-mappings for a user. + """Get available client role-mappings for a user. :param user_id: id of user :param client_id: id of client (not client-id) @@ -1840,8 +1763,7 @@ class KeycloakAdmin: ) def get_composite_client_roles_of_user(self, user_id, client_id): - """ - Get composite client role-mappings for a user. + """Get composite client role-mappings for a user. :param user_id: id of user :param client_id: id of client (not client-id) @@ -1857,8 +1779,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def delete_client_roles_of_user(self, user_id, client_id, roles): - """ - Delete client roles from a user. + """Delete client roles from a user. :param user_id: id of user :param client_id: id of client containing role (not client-id) @@ -1874,8 +1795,9 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_authentication_flows(self): - """ - Get authentication flows. Returns all flow details + """Get authentication flows. + + Returns all flow details AuthenticationFlowRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation @@ -1887,8 +1809,9 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_authentication_flow_for_id(self, flow_id): - """ - Get one authentication flow by it's id. Returns all flow details + """Get one authentication flow by it's id. + + Returns all flow details AuthenticationFlowRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation @@ -1901,8 +1824,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def create_authentication_flow(self, payload, skip_exists=False): - """ - Create a new authentication flow + """Create a new authentication flow. AuthenticationFlowRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation @@ -1911,7 +1833,6 @@ class KeycloakAdmin: :param skip_exists: Do not raise an error if authentication flow already exists :return: Keycloak server response (RoleRepresentation) """ - params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( urls_patterns.URL_ADMIN_FLOWS.format(**params_path), data=json.dumps(payload) @@ -1921,15 +1842,14 @@ class KeycloakAdmin: ) def copy_authentication_flow(self, payload, flow_alias): - """ - Copy existing authentication flow under a new name. The new name is given as 'newName' - attribute of the passed payload. + """Copy existing authentication flow under a new name. + + The new name is given as 'newName' attribute of the passed payload. :param payload: JSON containing 'newName' attribute :param flow_alias: the flow alias :return: Keycloak server response (RoleRepresentation) """ - params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post( urls_patterns.URL_ADMIN_FLOWS_COPY.format(**params_path), data=json.dumps(payload) @@ -1937,8 +1857,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def delete_authentication_flow(self, flow_id): - """ - Delete authentication flow + """Delete authentication flow. AuthenticationInfoRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationinforepresentation @@ -1951,8 +1870,9 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_authentication_flow_executions(self, flow_alias): - """ - Get authentication flow executions. Returns all execution steps + """Get authentication flow executions. + + Returns all execution steps :param flow_alias: the flow alias :return: Response(json) @@ -1962,8 +1882,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def update_authentication_flow_executions(self, payload, flow_alias): - """ - Update an authentication flow execution + """Update an authentication flow execution. AuthenticationExecutionInfoRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation @@ -1972,7 +1891,6 @@ class KeycloakAdmin: :param flow_alias: The flow alias :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_put( urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), @@ -1981,8 +1899,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[202, 204]) def get_authentication_flow_execution(self, execution_id): - """ - Get authentication flow execution. + """Get authentication flow execution. AuthenticationExecutionInfoRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation @@ -1995,8 +1912,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def create_authentication_flow_execution(self, payload, flow_alias): - """ - Create an authentication flow execution + """Create an authentication flow execution. AuthenticationExecutionInfoRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation @@ -2005,7 +1921,6 @@ class KeycloakAdmin: :param flow_alias: The flow alias :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post( urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION.format(**params_path), @@ -2014,8 +1929,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def delete_authentication_flow_execution(self, execution_id): - """ - Delete authentication flow execution + """Delete authentication flow execution. AuthenticationExecutionInfoRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation @@ -2028,8 +1942,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False): - """ - Create a new sub authentication flow for a given authentication flow + """Create a new sub authentication flow for a given authentication flow. AuthenticationFlowRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation @@ -2039,7 +1952,6 @@ class KeycloakAdmin: :param skip_exists: Do not raise an error if authentication flow already exists :return: Keycloak server response (RoleRepresentation) """ - params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post( urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), @@ -2050,8 +1962,7 @@ class KeycloakAdmin: ) def get_authenticator_providers(self): - """ - Get authenticator providers list. + """Get authenticator providers list. :return: Response(json) """ @@ -2062,8 +1973,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_authenticator_provider_config_description(self, provider_id): - """ - Get authenticator's provider configuration description. + """Get authenticator's provider configuration description. AuthenticatorConfigInfoRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticatorconfiginforepresentation @@ -2078,8 +1988,9 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_authenticator_config(self, config_id): - """ - Get authenticator configuration. Returns all configuration details. + """Get authenticator configuration. + + Returns all configuration details. :param config_id: Authenticator config id :return: Response(json) @@ -2089,8 +2000,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def update_authenticator_config(self, payload, config_id): - """ - Update an authenticator configuration. + """Update an authenticator configuration. AuthenticatorConfigRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticatorconfigrepresentation @@ -2107,14 +2017,13 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_authenticator_config(self, config_id): - """ - Delete a authenticator configuration. + """Delete a authenticator configuration. + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authentication_management_resource :param config_id: Authenticator config id :return: Keycloak server Response """ - params_path = {"realm-name": self.realm_name, "id": config_id} data_raw = self.raw_delete( urls_patterns.URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path) @@ -2122,8 +2031,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def sync_users(self, storage_id, action): - """ - Function to trigger user sync from provider + """Trigger user sync from provider. :param storage_id: The id of the user storage provider :param action: Action can be "triggerFullSync" or "triggerChangedUsersSync" @@ -2141,39 +2049,39 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError) def get_client_scopes(self): - """ + """Get client scopes. + Get representation of the client scopes for the realm where we are connected to https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes :return: Keycloak server response Array of (ClientScopeRepresentation) """ - params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SCOPES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_scope(self, client_scope_id): - """ + """Get client scope. + Get representation of the client scopes for the realm where we are connected to https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes :param client_scope_id: The id of the client scope :return: Keycloak server response (ClientScopeRepresentation) """ - params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_scope_by_name(self, client_scope_name): - """ + """Get client scope by name. + Get representation of the client scope identified by the client scope name. https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes :param client_scope_name: (str) Name of the client scope :returns: ClientScopeRepresentation or None """ - client_scopes = self.get_client_scopes() for client_scope in client_scopes: if client_scope["name"] == client_scope_name: @@ -2182,8 +2090,7 @@ class KeycloakAdmin: return None def create_client_scope(self, payload, skip_exists=False): - """ - Create a client scope + """Create a client scope. ClientScopeRepresentation: https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes @@ -2192,7 +2099,6 @@ class KeycloakAdmin: :param skip_exists: If true then do not raise an error if client scope already exists :return: Client scope id """ - if skip_exists: exists = self.get_client_scope_by_name(client_scope_name=payload["name"]) @@ -2210,8 +2116,7 @@ class KeycloakAdmin: return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def update_client_scope(self, client_scope_id, payload): - """ - Update a client scope + """Update a client scope. ClientScopeRepresentation: https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_client_scopes_resource @@ -2220,7 +2125,6 @@ class KeycloakAdmin: :param payload: ClientScopeRepresentation :return: Keycloak server response (ClientScopeRepresentation) """ - params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_put( urls_patterns.URL_ADMIN_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) @@ -2228,8 +2132,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_client_scope(self, client_scope_id): - """ - Delete existing client scope. + """Delete existing client scope. ClientScopeRepresentation: https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_client_scopes_resource @@ -2242,8 +2145,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_mappers_from_client_scope(self, client_scope_id): - """ - Get a list of all mappers connected to the client scope + """Get a list of all mappers connected to the client scope. https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_protocol_mappers_resource :param client_scope_id: Client scope id @@ -2256,15 +2158,14 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) def add_mapper_to_client_scope(self, client_scope_id, payload): - """ - Add a mapper to a client scope + """Add a mapper to a client scope. + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_create_mapper :param client_scope_id: The id of the client scope :param payload: ProtocolMapperRepresentation :return: Keycloak server Response """ - params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_post( @@ -2275,15 +2176,14 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def delete_mapper_from_client_scope(self, client_scope_id, protocol_mapper_id): - """ - Delete a mapper from a client scope + """Delete a mapper from a client scope. + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_delete_mapper :param client_scope_id: The id of the client scope :param protocol_mapper_id: Protocol mapper id :return: Keycloak server Response """ - params_path = { "realm-name": self.realm_name, "scope-id": client_scope_id, @@ -2296,8 +2196,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def update_mapper_in_client_scope(self, client_scope_id, protocol_mapper_id, payload): - """ - Update an existing protocol mapper in a client scope + """Update an existing protocol mapper in a client scope. + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_protocol_mappers_resource :param client_scope_id: The id of the client scope @@ -2306,7 +2206,6 @@ class KeycloakAdmin: :param payload: ProtocolMapperRepresentation :return: Keycloak server Response """ - params_path = { "realm-name": self.realm_name, "scope-id": client_scope_id, @@ -2321,7 +2220,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_default_default_client_scopes(self): - """ + """Get default default client scopes. + Return list of default default client scopes :return: Keycloak server response @@ -2333,8 +2233,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def delete_default_default_client_scope(self, scope_id): - """ - Delete default default client scope + """Delete default default client scope. :param scope_id: default default client scope id :return: Keycloak server response @@ -2346,8 +2245,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_default_default_client_scope(self, scope_id): - """ - Add default default client scope + """Add default default client scope. :param scope_id: default default client scope id :return: Keycloak server response @@ -2361,7 +2259,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_default_optional_client_scopes(self): - """ + """Get default optional client scopes. + Return list of default optional client scopes :return: Keycloak server response @@ -2373,8 +2272,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def delete_default_optional_client_scope(self, scope_id): - """ - Delete default optional client scope + """Delete default optional client scope. :param scope_id: default optional client scope id :return: Keycloak server response @@ -2386,8 +2284,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_default_optional_client_scope(self, scope_id): - """ - Add default optional client scope + """Add default optional client scope. :param scope_id: default optional client scope id :return: Keycloak server response @@ -2401,8 +2298,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_mappers_from_client(self, client_id): - """ - List of all client mappers. + """List of all client mappers. https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_protocolmapperrepresentation @@ -2418,15 +2314,14 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[200]) def add_mapper_to_client(self, client_id, payload): - """ - Add a mapper to a client + """Add a mapper to a client. + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_create_mapper :param client_id: The id of the client :param payload: ProtocolMapperRepresentation :return: Keycloak server Response """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( @@ -2437,14 +2332,13 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def update_client_mapper(self, client_id, mapper_id, payload): - """ - Update client mapper + """Update client mapper. + :param client_id: The id of the client :param client_mapper_id: The id of the mapper to be deleted :param payload: ProtocolMapperRepresentation :return: Keycloak server response """ - params_path = { "realm-name": self.realm_name, "id": client_id, @@ -2459,14 +2353,13 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def remove_client_mapper(self, client_id, client_mapper_id): - """ - Removes a mapper from the client + """Remove a mapper from the client. + https://www.keycloak.org/docs-api/15.0/rest-api/index.html#_protocol_mappers_resource :param client_id: The id of the client :param client_mapper_id: The id of the mapper to be deleted :return: Keycloak server response """ - params_path = { "realm-name": self.realm_name, "id": client_id, @@ -2479,15 +2372,13 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def generate_client_secrets(self, client_id): - """ + """Generate a new secret for the client. - Generate a new secret for the client https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_regeneratesecret :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( urls_patterns.URL_ADMIN_CLIENT_SECRETS.format(**params_path), data=None @@ -2495,21 +2386,20 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError) def get_client_secrets(self, client_id): - """ + """Get representation of the client secrets. - Get representation of the client secrets https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientsecret :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SECRETS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_components(self, query=None): - """ + """Get components. + Return a list of components, filtered according to query parameters ComponentRepresentation @@ -2526,8 +2416,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def create_component(self, payload): - """ - Create a new component. + """Create a new component. ComponentRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation @@ -2544,8 +2433,7 @@ class KeycloakAdmin: return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def get_component(self, component_id): - """ - Get representation of the component + """Get representation of the component. :param component_id: Component id @@ -2559,8 +2447,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def update_component(self, component_id, payload): - """ - Update the component + """Update the component. :param component_id: Component id :param payload: ComponentRepresentation @@ -2575,8 +2462,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_component(self, component_id): - """ - Delete the component + """Delete the component. :param component_id: Component id @@ -2587,7 +2473,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_keys(self): - """ + """Get keys. + Return a list of keys, filtered according to query parameters KeysMetadataRepresentation @@ -2600,7 +2487,8 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_events(self, query=None): - """ + """Get events. + Return a list of events, filtered according to query parameters EventRepresentation array @@ -2616,8 +2504,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def set_events(self, payload): - """ - Set realm events configuration + """Set realm events configuration. RealmEventsConfigRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmeventsconfigrepresentation @@ -2631,8 +2518,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def raw_get(self, *args, **kwargs): - """ - Calls connection.raw_get. + """Call connection.raw_get. If auto_refresh is set for *get* and *access_token* is expired, it will refresh the token and try *get* once more. @@ -2644,8 +2530,7 @@ class KeycloakAdmin: return r def raw_post(self, *args, **kwargs): - """ - Calls connection.raw_post. + """Call connection.raw_post. If auto_refresh is set for *post* and *access_token* is expired, it will refresh the token and try *post* once more. @@ -2657,8 +2542,7 @@ class KeycloakAdmin: return r def raw_put(self, *args, **kwargs): - """ - Calls connection.raw_put. + """Call connection.raw_put. If auto_refresh is set for *put* and *access_token* is expired, it will refresh the token and try *put* once more. @@ -2670,8 +2554,7 @@ class KeycloakAdmin: return r def raw_delete(self, *args, **kwargs): - """ - Calls connection.raw_delete. + """Call connection.raw_delete. If auto_refresh is set for *delete* and *access_token* is expired, it will refresh the token and try *delete* once more. @@ -2683,6 +2566,7 @@ class KeycloakAdmin: return r def get_token(self): + """Get admin token.""" if self.user_realm_name: token_realm_name = self.user_realm_name elif self.realm_name: @@ -2730,6 +2614,7 @@ class KeycloakAdmin: ) def refresh_token(self): + """Refresh the token.""" refresh_token = self.token.get("refresh_token", None) if refresh_token is None: self.get_token() @@ -2752,8 +2637,7 @@ class KeycloakAdmin: ) def get_client_all_sessions(self, client_id): - """ - Get sessions associated with the client + """Get sessions associated with the client. :param client_id: id of client @@ -2767,8 +2651,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_client_sessions_stats(self): - """ - Get current session count for all clients with active sessions + """Get current session count for all clients with active sessions. https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientsessionstats @@ -2779,8 +2662,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_client_management_permissions(self, client_id): - """ - Get management permissions for a client. + """Get management permissions for a client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -2793,8 +2675,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def update_client_management_permissions(self, payload, client_id): - """ - Update management permissions for a client. + """Update management permissions for a client. ManagementPermissionReference https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_managementpermissionreference @@ -2819,8 +2700,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[200]) def get_client_authz_policy_scopes(self, client_id, policy_id): - """ - Get scopes for a given policy. + """Get scopes for a given policy. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -2834,8 +2714,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_policy_resources(self, client_id, policy_id): - """ - Get resources for a given policy. + """Get resources for a given policy. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -2849,8 +2728,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_scope_permission(self, client_id, scope_id): - """ - Get permissions for a given scope. + """Get permissions for a given scope. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -2864,8 +2742,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError) def update_client_authz_scope_permission(self, payload, client_id, scope_id): - """ - Update permissions for a given scope. + """Update permissions for a given scope. :param payload: No Document :param client_id: id in ClientRepresentation @@ -2895,8 +2772,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[201]) def get_client_authz_client_policies(self, client_id): - """ - Get policies for a given client. + """Get policies for a given client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -2909,8 +2785,7 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) def create_client_authz_client_policy(self, payload, client_id): - """ - Create a new policy for a given client. + """Create a new policy for a given client. :param payload: No Document :param client_id: id in ClientRepresentation diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index fa04e4d..83575b2 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -21,6 +21,12 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Keycloak OpenID module. + +The module contains mainly the implementation of KeycloakOpenID class, the main +class to handle authentication and token manipulation. +""" + import json from jose import jwt @@ -52,8 +58,7 @@ from .urls_patterns import ( class KeycloakOpenID: - """ - Keycloak OpenID client. + """Keycloak OpenID client. :param server_url: Keycloak server url :param client_id: client id @@ -75,6 +80,7 @@ class KeycloakOpenID: proxies=None, timeout=60, ): + """Init method.""" self.client_id = client_id self.client_secret_key = client_secret_key self.realm_name = realm_name @@ -87,6 +93,7 @@ class KeycloakOpenID: @property def client_id(self): + """Get client id.""" return self._client_id @client_id.setter @@ -95,6 +102,7 @@ class KeycloakOpenID: @property def client_secret_key(self): + """Get the client secret key.""" return self._client_secret_key @client_secret_key.setter @@ -103,6 +111,7 @@ class KeycloakOpenID: @property def realm_name(self): + """Get the realm name.""" return self._realm_name @realm_name.setter @@ -111,6 +120,7 @@ class KeycloakOpenID: @property def connection(self): + """Get connection.""" return self._connection @connection.setter @@ -119,6 +129,7 @@ class KeycloakOpenID: @property def authorization(self): + """Get authorization.""" return self._authorization @authorization.setter @@ -126,8 +137,7 @@ class KeycloakOpenID: self._authorization = value def _add_secret_key(self, payload): - """ - Add secret key if exist. + """Add secret key if exists. :param payload: :return: @@ -138,7 +148,7 @@ class KeycloakOpenID: return payload def _build_name_role(self, role): - """ + """Build name of a role. :param role: :return: @@ -146,7 +156,7 @@ class KeycloakOpenID: return self.client_id + "/" + role def _token_info(self, token, method_token_info, **kwargs): - """ + """Getter for the token data. :param token: :param method_token_info: @@ -161,19 +171,20 @@ class KeycloakOpenID: return token_info def well_known(self): - """The most important endpoint to understand is the well-known configuration + """Get the well_known object. + + The most important endpoint to understand is the well-known configuration endpoint. It lists endpoints and other configuration options relevant to the OpenID Connect implementation in Keycloak. :return It lists endpoints and other configuration options relevant. """ - params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_get(URL_WELL_KNOWN.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def auth_url(self, redirect_uri): - """ + """Get the authentication URL endpoint. http://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint @@ -196,7 +207,8 @@ class KeycloakOpenID: totp=None, **extra ): - """ + """Retrieve user token. + The token endpoint is used to obtain tokens. Tokens can either be obtained by exchanging an authorization code or by supplying credentials directly depending on what flow is used. The token endpoint is also used to obtain new access tokens @@ -232,7 +244,8 @@ class KeycloakOpenID: return raise_error_from_response(data_raw, KeycloakPostError) 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 exchanging an authorization code or by supplying credentials directly depending on what flow is used. The token endpoint is also used to obtain new access tokens @@ -255,7 +268,8 @@ class KeycloakOpenID: return raise_error_from_response(data_raw, KeycloakPostError) def exchange_token(self, token: str, client_id: str, audience: str, subject: str) -> dict: - """ + """Exchange user token. + Use a token to obtain an entirely different token. See https://www.keycloak.org/docs/latest/securing_apps/index.html#_token-exchange @@ -279,7 +293,8 @@ class KeycloakOpenID: return raise_error_from_response(data_raw, KeycloakPostError) def userinfo(self, token): - """ + """Get the user info object. + The userinfo endpoint returns standard claims about the authenticated user, and is protected by a bearer token. @@ -294,8 +309,8 @@ class KeycloakOpenID: return raise_error_from_response(data_raw, KeycloakGetError) def logout(self, refresh_token): - """ - The logout endpoint logs out the authenticated user. + """Log out the authenticated user. + :param refresh_token: :return: """ @@ -306,7 +321,8 @@ class KeycloakOpenID: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def certs(self): - """ + """Get certificates. + The certificate endpoint returns the public keys enabled by the realm, encoded as a JSON Web Key (JWK). Depending on the realm settings there can be one or more keys enabled for verifying tokens. @@ -320,7 +336,8 @@ class KeycloakOpenID: return raise_error_from_response(data_raw, KeycloakGetError) def public_key(self): - """ + """Retrieve the public key. + The public key is exposed by the realm page directly. :return: @@ -330,7 +347,8 @@ class KeycloakOpenID: return raise_error_from_response(data_raw, KeycloakGetError)["public_key"] 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 called a requesting party token (RPT). This token consists of all the entitlements (or permissions) for a user as a result of the evaluation of the permissions and @@ -349,7 +367,8 @@ class KeycloakOpenID: return raise_error_from_response(data_raw, KeycloakGetError) # pragma: no cover 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. It is can only be invoked by confidential clients. @@ -377,7 +396,8 @@ class KeycloakOpenID: return raise_error_from_response(data_raw, KeycloakPostError) def decode_token(self, token, key, algorithms=["RS256"], **kwargs): - """ + """Decode user token. + A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key. This specification also defines a JWK Set JSON data structure that represents a set of @@ -395,8 +415,7 @@ class KeycloakOpenID: return jwt.decode(token, key, algorithms=algorithms, audience=self.client_id, **kwargs) def load_authorization_config(self, path): - """ - Load Keycloak settings (authorization) + """Load Keycloak settings (authorization). :param path: settings file (json) :return: @@ -407,8 +426,7 @@ class KeycloakOpenID: self.authorization.load_config(authorization_json) def get_policies(self, token, method_token_info="introspect", **kwargs): - """ - Get policies by user token + """Get policies by user token. :param token: user token :return: policies list @@ -438,8 +456,7 @@ class KeycloakOpenID: return list(set(policies)) 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 method_token_info: Decode token method @@ -471,8 +488,7 @@ class KeycloakOpenID: return list(set(permissions)) 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 invoked by confidential clients. @@ -499,8 +515,7 @@ class KeycloakOpenID: return raise_error_from_response(data_raw, KeycloakPostError) 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 permissions: list of uma permissions (resource:scope) diff --git a/src/keycloak/uma_permissions.py b/src/keycloak/uma_permissions.py index 5653c76..1bf2136 100644 --- a/src/keycloak/uma_permissions.py +++ b/src/keycloak/uma_permissions.py @@ -21,11 +21,14 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""User-managed access permissions module.""" + from keycloak.exceptions import KeycloakPermissionFormatError, PermissionDefinitionError class UMAPermission: """A class to conveniently assembly permissions. + The class itself is callable, and will return the assembled permission. Usage example: @@ -36,9 +39,16 @@ class UMAPermission: >>> print(permission) 'Users#delete' + :param permission: Permission + :type permission: UMAPermission + :param resource: Resource + :type resource: str + :param scope: Scope + :type scope: str """ def __init__(self, permission=None, resource="", scope=""): + """Init method.""" self.resource = resource self.scope = scope @@ -53,21 +63,26 @@ class UMAPermission: self.scope = str(permission.scope) def __str__(self): + """Str method.""" scope = self.scope if scope: scope = "#" + scope return "{}{}".format(self.resource, scope) def __eq__(self, __o: object) -> bool: + """Eq method.""" return str(self) == str(__o) def __repr__(self) -> str: + """Repr method.""" return self.__str__() def __hash__(self) -> int: + """Hash method.""" return hash(str(self)) def __call__(self, permission=None, resource="", scope="") -> object: + """Call method.""" result_resource = self.resource result_scope = self.scope @@ -91,36 +106,58 @@ class UMAPermission: class Resource(UMAPermission): """An UMAPermission Resource class to conveniently assembly permissions. + The class itself is callable, and will return the assembled permission. + + :param resource: Resource + :type resource: str """ def __init__(self, resource): + """Init method.""" super().__init__(resource=resource) class Scope(UMAPermission): """An UMAPermission Scope class to conveniently assembly permissions. + The class itself is callable, and will return the assembled permission. + + :param scope: Scope + :type scope: str """ def __init__(self, scope): + """Init method.""" super().__init__(scope=scope) class AuthStatus: """A class that represents the authorization/login status of a user associated with a token. + This has to evaluate to True if and only if the user is properly authorized - for the requested resource.""" + for the requested resource. + + :param is_logged_in: Is logged in indicator + :type is_logged_in: bool + :param is_authorized: Is authorized indicator + :type is_authorized: bool + :param missing_permissions: Missing permissions + :type missing_permissions: set + """ def __init__(self, is_logged_in, is_authorized, missing_permissions): + """Init method.""" self.is_logged_in = is_logged_in self.is_authorized = is_authorized self.missing_permissions = missing_permissions def __bool__(self): + """Bool method.""" return self.is_authorized def __repr__(self): + """Repr method.""" return ( f"AuthStatus(" f"is_authorized={self.is_authorized}, " @@ -130,8 +167,7 @@ class AuthStatus: def build_permission_param(permissions): - """ - Transform permissions to a set, so they are usable for requests + """Transform permissions to a set, so they are usable for requests. :param permissions: either str (resource#scope), iterable[str] (resource#scope), diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 3ec134c..b836692 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -21,6 +21,8 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Keycloak URL patterns.""" + # OPENID URLS URL_REALM = "realms/{realm-name}" URL_WELL_KNOWN = "realms/{realm-name}/.well-known/openid-configuration" diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..f1b390f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests module.""" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index e62bdda..069bb61 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1,3 +1,5 @@ +"""Test the keycloak admin object.""" + import pytest import keycloak @@ -13,10 +15,12 @@ from keycloak.exceptions import ( def test_keycloak_version(): + """Test version.""" assert keycloak.__version__, keycloak.__version__ def test_keycloak_admin_bad_init(env): + """Test keycloak admin bad init.""" with pytest.raises(TypeError) as err: KeycloakAdmin( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", @@ -37,6 +41,7 @@ def test_keycloak_admin_bad_init(env): def test_keycloak_admin_init(env): + """Test keycloak admin init.""" admin = KeycloakAdmin( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", username=env.KEYCLOAK_ADMIN, @@ -111,6 +116,7 @@ def test_keycloak_admin_init(env): def test_realms(admin: KeycloakAdmin): + """Test realms.""" # Get realms realms = admin.get_realms() assert len(realms) == 1, realms @@ -175,6 +181,7 @@ def test_realms(admin: KeycloakAdmin): def test_import_export_realms(admin: KeycloakAdmin, realm: str): + """Test import and export of realms.""" admin.realm_name = realm realm_export = admin.export_realm(export_clients=True, export_groups_and_role=True) @@ -192,6 +199,7 @@ def test_import_export_realms(admin: KeycloakAdmin, realm: str): def test_users(admin: KeycloakAdmin, realm: str): + """Test users.""" admin.realm_name = realm # Check no users present @@ -283,6 +291,7 @@ def test_users(admin: KeycloakAdmin, realm: str): def test_users_pagination(admin: KeycloakAdmin, realm: str): + """Test user pagination.""" admin.realm_name = realm for ind in range(admin.PAGE_SIZE + 50): @@ -300,6 +309,7 @@ def test_users_pagination(admin: KeycloakAdmin, realm: str): def test_idps(admin: KeycloakAdmin, realm: str): + """Test IDPs.""" admin.realm_name = realm # Create IDP @@ -371,6 +381,7 @@ def test_idps(admin: KeycloakAdmin, realm: str): def test_user_credentials(admin: KeycloakAdmin, user: str): + """Test user credentials.""" res = admin.set_user_password(user_id=user, password="booya", temporary=True) assert res == dict(), res @@ -398,6 +409,7 @@ def test_user_credentials(admin: KeycloakAdmin, user: str): def test_social_logins(admin: KeycloakAdmin, user: str): + """Test social logins.""" res = admin.add_user_social_login( user_id=user, provider_id="gitlab", provider_userid="test", provider_username="test" ) @@ -437,6 +449,7 @@ def test_social_logins(admin: KeycloakAdmin, user: str): def test_server_info(admin: KeycloakAdmin): + """Test server info.""" info = admin.get_server_info() assert set(info.keys()) == { "systemInfo", @@ -456,6 +469,7 @@ def test_server_info(admin: KeycloakAdmin): def test_groups(admin: KeycloakAdmin, user: str): + """Test groups.""" # Test get groups groups = admin.get_groups() assert len(groups) == 0 @@ -599,6 +613,7 @@ def test_groups(admin: KeycloakAdmin, user: str): def test_clients(admin: KeycloakAdmin, realm: str): + """Test clients.""" admin.realm_name = realm # Test get clients @@ -860,6 +875,7 @@ def test_clients(admin: KeycloakAdmin, realm: str): def test_realm_roles(admin: KeycloakAdmin, realm: str): + """Test realm roles.""" admin.realm_name = realm # Test get realm roles @@ -1015,6 +1031,7 @@ def test_realm_roles(admin: KeycloakAdmin, realm: str): def test_client_roles(admin: KeycloakAdmin, client: str): + """Test client roles.""" # Test get client roles res = admin.get_client_roles(client_id=client) assert len(res) == 0 @@ -1177,6 +1194,7 @@ def test_client_roles(admin: KeycloakAdmin, client: str): def test_enable_token_exchange(admin: KeycloakAdmin, realm: str): + """Test enable token exchange.""" # Test enabling token exchange between two confidential clients admin.realm_name = realm @@ -1265,6 +1283,7 @@ def test_enable_token_exchange(admin: KeycloakAdmin, realm: str): def test_email(admin: KeycloakAdmin, user: str): + """Test email.""" # Emails will fail as we don't have SMTP test setup with pytest.raises(KeycloakPutError) as err: admin.send_update_account(user_id=user, payload=dict()) @@ -1277,6 +1296,7 @@ def test_email(admin: KeycloakAdmin, user: str): def test_get_sessions(admin: KeycloakAdmin): + """Test get sessions.""" sessions = admin.get_sessions(user_id=admin.get_user_id(username=admin.username)) assert len(sessions) >= 1 with pytest.raises(KeycloakGetError) as err: @@ -1285,6 +1305,7 @@ def test_get_sessions(admin: KeycloakAdmin): def test_get_client_installation_provider(admin: KeycloakAdmin, client: str): + """Test get client installation provider.""" with pytest.raises(KeycloakGetError) as err: admin.get_client_installation_provider(client_id=client, provider_id="bad") assert err.match('404: b\'{"error":"Unknown Provider"}\'') @@ -1303,6 +1324,7 @@ def test_get_client_installation_provider(admin: KeycloakAdmin, client: str): def test_auth_flows(admin: KeycloakAdmin, realm: str): + """Test auth flows.""" admin.realm_name = realm res = admin.get_authentication_flows() @@ -1449,6 +1471,7 @@ def test_auth_flows(admin: KeycloakAdmin, realm: str): def test_authentication_configs(admin: KeycloakAdmin, realm: str): + """Test authentication configs.""" admin.realm_name = realm # Test list of auth providers @@ -1480,6 +1503,7 @@ def test_authentication_configs(admin: KeycloakAdmin, realm: str): def test_sync_users(admin: KeycloakAdmin, realm: str): + """Test sync users.""" admin.realm_name = realm # Only testing the error message @@ -1489,6 +1513,7 @@ def test_sync_users(admin: KeycloakAdmin, realm: str): def test_client_scopes(admin: KeycloakAdmin, realm: str): + """Test client scopes.""" admin.realm_name = realm # Test get client scopes @@ -1626,6 +1651,7 @@ def test_client_scopes(admin: KeycloakAdmin, realm: str): def test_components(admin: KeycloakAdmin, realm: str): + """Test components.""" admin.realm_name = realm # Test get components @@ -1676,6 +1702,7 @@ def test_components(admin: KeycloakAdmin, realm: str): def test_keys(admin: KeycloakAdmin, realm: str): + """Test keys.""" admin.realm_name = realm assert set(admin.get_keys()["active"].keys()) == {"AES", "HS256", "RS256", "RSA-OAEP"} assert {k["algorithm"] for k in admin.get_keys()["keys"]} == { @@ -1687,6 +1714,7 @@ def test_keys(admin: KeycloakAdmin, realm: str): def test_events(admin: KeycloakAdmin, realm: str): + """Test events.""" admin.realm_name = realm events = admin.get_events() @@ -1706,6 +1734,7 @@ def test_events(admin: KeycloakAdmin, realm: str): def test_auto_refresh(admin: KeycloakAdmin, realm: str): + """Test auto refresh token.""" # Test get refresh admin.auto_refresh_token = list() admin.connection = ConnectionManager( diff --git a/tests/test_uma_permissions.py b/tests/test_uma_permissions.py index 09d7147..581faf4 100644 --- a/tests/test_uma_permissions.py +++ b/tests/test_uma_permissions.py @@ -14,6 +14,9 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . + +"""Test uma permissions.""" + import re import pytest @@ -23,30 +26,35 @@ from keycloak.uma_permissions import Resource, Scope, build_permission_param def test_resource_with_scope_obj(): + """Test resource with scope.""" r = Resource("Resource1") s = Scope("Scope1") assert r(s) == "Resource1#Scope1" def test_scope_with_resource_obj(): + """Test scope with resource.""" r = Resource("Resource1") s = Scope("Scope1") assert s(r) == "Resource1#Scope1" def test_resource_scope_str(): + """Test resource scope as string.""" r = Resource("Resource1") s = "Scope1" assert r(scope=s) == "Resource1#Scope1" def test_scope_resource_str(): + """Test scope resource as string.""" r = "Resource1" s = Scope("Scope1") assert s(resource=r) == "Resource1#Scope1" def test_resource_scope_list(): + """Test resource scope as list.""" r = Resource("Resource1") s = ["Scope1"] with pytest.raises(PermissionDefinitionError) as err: @@ -55,94 +63,114 @@ def test_resource_scope_list(): def test_build_permission_none(): + """Test build permission param with None.""" assert build_permission_param(None) == set() def test_build_permission_empty_str(): + """Test build permission param with an empty string.""" assert build_permission_param("") == set() def test_build_permission_empty_list(): + """Test build permission param with an empty list.""" assert build_permission_param([]) == set() def test_build_permission_empty_tuple(): + """Test build permission param with an empty tuple.""" assert build_permission_param(()) == set() def test_build_permission_empty_set(): + """Test build permission param with an empty set.""" assert build_permission_param(set()) == set() def test_build_permission_empty_dict(): + """Test build permission param with an empty dict.""" assert build_permission_param({}) == set() def test_build_permission_str(): + """Test build permission param as string.""" assert build_permission_param("resource1") == {"resource1"} def test_build_permission_list_str(): + """Test build permission param with list of strings.""" assert build_permission_param(["res1#scope1", "res1#scope2"]) == {"res1#scope1", "res1#scope2"} def test_build_permission_tuple_str(): + """Test build permission param with tuple of strings.""" assert build_permission_param(("res1#scope1", "res1#scope2")) == {"res1#scope1", "res1#scope2"} def test_build_permission_set_str(): + """Test build permission param with set of strings.""" assert build_permission_param({"res1#scope1", "res1#scope2"}) == {"res1#scope1", "res1#scope2"} def test_build_permission_tuple_dict_str_str(): + """Test build permission param with dictionary.""" assert build_permission_param({"res1": "scope1"}) == {"res1#scope1"} def test_build_permission_tuple_dict_str_list_str(): + """Test build permission param with dictionary of list.""" assert build_permission_param({"res1": ["scope1", "scope2"]}) == {"res1#scope1", "res1#scope2"} def test_build_permission_tuple_dict_str_list_str2(): + """Test build permission param with mutliple-keyed dictionary.""" assert build_permission_param( {"res1": ["scope1", "scope2"], "res2": ["scope2", "scope3"]} ) == {"res1#scope1", "res1#scope2", "res2#scope2", "res2#scope3"} def test_build_permission_uma(): + """Test build permission param with UMA.""" assert build_permission_param(Resource("res1")(Scope("scope1"))) == {"res1#scope1"} def test_build_permission_uma_list(): + """Test build permission param with list of UMAs.""" assert build_permission_param( [Resource("res1")(Scope("scope1")), Resource("res1")(Scope("scope2"))] ) == {"res1#scope1", "res1#scope2"} def test_build_permission_misbuilt_dict_str_list_list_str(): + """Test bad build of permission param from dictionary.""" with pytest.raises(KeycloakPermissionFormatError) as err: build_permission_param({"res1": [["scope1", "scope2"]]}) assert err.match(re.escape("misbuilt permission {'res1': [['scope1', 'scope2']]}")) def test_build_permission_misbuilt_list_list_str(): + """Test bad build of permission param from list.""" with pytest.raises(KeycloakPermissionFormatError) as err: print(build_permission_param([["scope1", "scope2"]])) assert err.match(re.escape("misbuilt permission [['scope1', 'scope2']]")) def test_build_permission_misbuilt_list_set_str(): + """Test bad build of permission param from set.""" with pytest.raises(KeycloakPermissionFormatError) as err: build_permission_param([{"scope1", "scope2"}]) assert err.match("misbuilt permission.*") def test_build_permission_misbuilt_set_set_str(): + """Test bad build of permission param from list of set.""" with pytest.raises(KeycloakPermissionFormatError) as err: build_permission_param([{"scope1"}]) assert err.match(re.escape("misbuilt permission [{'scope1'}]")) def test_build_permission_misbuilt_dict_non_iterable(): + """Test bad build of permission param from non-iterable.""" with pytest.raises(KeycloakPermissionFormatError) as err: build_permission_param({"res1": 5}) assert err.match(re.escape("misbuilt permission {'res1': 5}")) diff --git a/tests/test_urls_patterns.py b/tests/test_urls_patterns.py index 6fa5a87..5fae847 100644 --- a/tests/test_urls_patterns.py +++ b/tests/test_urls_patterns.py @@ -1,9 +1,10 @@ +"""Test URL patterns.""" + from keycloak import urls_patterns def test_correctness_of_patterns(): """Test that there are no duplicate url patterns.""" - # Test that the patterns are present urls = [x for x in dir(urls_patterns) if not x.startswith("__")] assert len(urls) >= 0 From dfc7c4a2d52ab11b6a11ef1cff4bdd9585b5f473 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 12 Jul 2022 08:00:54 +0000 Subject: [PATCH 13/19] style: added docstring to conf --- docs/source/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index b60a1c9..9a5d438 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -20,6 +20,9 @@ # import os # import sys # sys.path.insert(0, os.path.abspath('.')) + +"""Sphinx documentation configuration.""" + import sphinx_rtd_theme # -- General configuration ------------------------------------------------ From 3a697caaef50254725d51fb052e999c1e9690a4b Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 12 Jul 2022 15:28:12 +0000 Subject: [PATCH 14/19] test: added load authorization config test --- src/keycloak/authorization/permission.py | 12 +++---- src/keycloak/authorization/policy.py | 20 +++++++---- tests/conftest.py | 8 +++++ tests/data/authz_settings.json | 45 ++++++++++++++++++++++++ tests/test_keycloak_openid.py | 38 +++++++++++++++++++- 5 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 tests/data/authz_settings.json diff --git a/src/keycloak/authorization/permission.py b/src/keycloak/authorization/permission.py index a444f83..667f8c3 100644 --- a/src/keycloak/authorization/permission.py +++ b/src/keycloak/authorization/permission.py @@ -49,12 +49,12 @@ class Permission: def __init__(self, name, type, logic, decision_strategy): """Init method.""" - self._name = name - self._type = type - self._logic = logic - self._decision_strategy = decision_strategy - self._resources = [] - self._scopes = [] + self.name = name + self.type = type + self.logic = logic + self.decision_strategy = decision_strategy + self.resources = [] + self.scopes = [] def __repr__(self): """Repr method.""" diff --git a/src/keycloak/authorization/policy.py b/src/keycloak/authorization/policy.py index 6b558d8..7e03db0 100644 --- a/src/keycloak/authorization/policy.py +++ b/src/keycloak/authorization/policy.py @@ -43,12 +43,12 @@ class Policy: def __init__(self, name, type, logic, decision_strategy): """Init method.""" - self._name = name - self._type = type - self._logic = logic - self._decision_strategy = decision_strategy - self._roles = [] - self._permissions = [] + self.name = name + self.type = type + self.logic = logic + self.decision_strategy = decision_strategy + self.roles = [] + self.permissions = [] def __repr__(self): """Repr method.""" @@ -99,11 +99,19 @@ class Policy: """Get roles.""" return self._roles + @roles.setter + def roles(self, value): + self._roles = value + @property def permissions(self): """Get permissions.""" return self._permissions + @permissions.setter + def permissions(self, value): + self._permissions = value + def add_role(self, role): """Add keycloak role in policy. diff --git a/tests/conftest.py b/tests/conftest.py index 47c9854..632c51b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -185,6 +185,14 @@ def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: Keycloak "serviceAccountsEnabled": True, } ) + 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"]}], + }, + ) + admin.create_client_authz_resource # Create user username = str(uuid.uuid4()) password = str(uuid.uuid4()) diff --git a/tests/data/authz_settings.json b/tests/data/authz_settings.json new file mode 100644 index 0000000..e051085 --- /dev/null +++ b/tests/data/authz_settings.json @@ -0,0 +1,45 @@ +{ + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "policies": [ + { + "name": "Default Policy", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "name": "test-authz-rb-policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"offline_access\",\"required\":false}]" + } + }, + { + "name": "Default Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "applyPolicies": "[\"test-authz-rb-policy\"]" + } + }, + { + "name": "Test scope", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "scopes": "[]", + "applyPolicies": "[\"test-authz-rb-policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" +} \ No newline at end of file diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 9ed2b88..f2c0f7e 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -4,8 +4,15 @@ from unittest import mock import pytest from keycloak.authorization import Authorization +from keycloak.authorization.permission import Permission +from keycloak.authorization.policy import Policy +from keycloak.authorization.role import Role from keycloak.connection import ConnectionManager -from keycloak.exceptions import KeycloakDeprecationError, KeycloakRPTNotFound +from keycloak.exceptions import ( + KeycloakAuthenticationError, + KeycloakDeprecationError, + KeycloakRPTNotFound, +) from keycloak.keycloak_admin import KeycloakAdmin from keycloak.keycloak_openid import KeycloakOpenID @@ -185,6 +192,18 @@ def test_exchange_token( assert token != new_token +def test_logout(oid_with_credentials): + """Test logout.""" + 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() + + with pytest.raises(KeycloakAuthenticationError): + oid.userinfo(token=token["access_token"]) + + def test_certs(oid: KeycloakOpenID): """Test certificates.""" assert len(oid.certs()["keys"]) == 2 @@ -236,3 +255,20 @@ def test_decode_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): )["preferred_username"] == username ) + + +def test_load_authorization_config( + oid_with_credentials_authz: tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin +): + """Test load authorization config.""" + oid, username, password = oid_with_credentials_authz + + oid.load_authorization_config(path="tests/data/authz_settings.json") + assert "test-authz-rb-policy" in oid.authorization.policies + assert isinstance(oid.authorization.policies["test-authz-rb-policy"], Policy) + assert len(oid.authorization.policies["test-authz-rb-policy"].roles) == 1 + assert isinstance(oid.authorization.policies["test-authz-rb-policy"].roles[0], Role) + assert len(oid.authorization.policies["test-authz-rb-policy"].permissions) == 2 + assert isinstance( + oid.authorization.policies["test-authz-rb-policy"].permissions[0], Permission + ) From 18ce10c73b9ded0d589098f803504477fad358f6 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 12 Jul 2022 20:34:24 +0000 Subject: [PATCH 15/19] test: added authz tests --- src/keycloak/keycloak_openid.py | 1 - tests/test_keycloak_openid.py | 83 +++++++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 83575b2..0a45dc3 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -511,7 +511,6 @@ class KeycloakOpenID: self.connection.add_param_headers("Authorization", "Bearer " + token) data_raw = 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): diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index f2c0f7e..0e94cd3 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -10,7 +10,9 @@ from keycloak.authorization.role import Role from keycloak.connection import ConnectionManager from keycloak.exceptions import ( KeycloakAuthenticationError, + KeycloakAuthorizationConfigError, KeycloakDeprecationError, + KeycloakInvalidTokenError, KeycloakRPTNotFound, ) from keycloak.keycloak_admin import KeycloakAdmin @@ -257,9 +259,7 @@ def test_decode_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): ) -def test_load_authorization_config( - oid_with_credentials_authz: tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin -): +def test_load_authorization_config(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): """Test load authorization config.""" oid, username, password = oid_with_credentials_authz @@ -272,3 +272,80 @@ def test_load_authorization_config( assert isinstance( oid.authorization.policies["test-authz-rb-policy"].permissions[0], Permission ) + + +def test_get_policies(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): + """Test get policies.""" + oid, username, password = oid_with_credentials_authz + token = oid.token(username=username, password=password) + + with pytest.raises(KeycloakAuthorizationConfigError): + 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 + + 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) == [] + 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) + ] == ["Policy: test (role)"] + assert [ + repr(x) + for x in oid.get_policies(token=token["access_token"], method_token_info="decode", key=key) + ] == [""] + oid.client_id = orig_client_id + + oid.logout(refresh_token=token["refresh_token"]) + with pytest.raises(KeycloakInvalidTokenError): + oid.get_policies(token=token["access_token"]) + + +def test_get_permissions(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): + """Test get policies.""" + oid, username, password = oid_with_credentials_authz + token = oid.token(username=username, password=password) + + with pytest.raises(KeycloakAuthorizationConfigError): + 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 + + 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) == [] + ) + policy = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS") + policy.add_role(role="account/view-profile") + policy.add_permission( + permission=Permission( + name="test-perm", type="resource", logic="POSITIVE", decision_strategy="UNANIMOUS" + ) + ) + oid.authorization.policies["test"] = policy + assert [ + str(x) + for x in 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( + token=token["access_token"], method_token_info="decode", key=key + ) + ] == [""] + oid.client_id = orig_client_id + + oid.logout(refresh_token=token["refresh_token"]) + with pytest.raises(KeycloakInvalidTokenError): + oid.get_permissions(token=token["access_token"]) From 25f1f687059ee780f9cb99eaf18ce821fb7943ef Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 12 Jul 2022 20:47:18 +0000 Subject: [PATCH 16/19] style: fix docstring for docs pages --- src/keycloak/keycloak_admin.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 0825730..52c5677 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -1313,7 +1313,7 @@ class KeycloakAdmin: :param role_name: Name of the role. :param query: Additional Query parameters - (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_roles_resource) + (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_roles_resource) :return: Keycloak Server Response (UserRepresentation) """ query = query or dict() @@ -1464,7 +1464,7 @@ class KeycloakAdmin: :param client_id: The client id :param role_name: the name of role to be queried. :param query: Additional query parameters - (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource) + (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource) :return: Keycloak server response (UserRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} @@ -1478,7 +1478,7 @@ class KeycloakAdmin: :param client_id: The client id :param role_name: the name of role to be queried. :param query: Additional query parameters - (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource) + (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource) :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} @@ -1530,7 +1530,7 @@ class KeycloakAdmin: :param role_name: The name of the role to be updated :param payload: The role (use RoleRepresentation) - :return Keycloak server response + :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_put( @@ -1543,7 +1543,7 @@ class KeycloakAdmin: """Delete a role for the realm by name. :param payload: The role name {'role-name':'name-of-the-role'} - :return Keycloak server response + :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_delete( From a07a5eb60f2c3cbb73be966c5141d8c91a7fd85e Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 12 Jul 2022 21:08:28 +0000 Subject: [PATCH 17/19] style: removed manifest, applied pre-commit hooks --- LICENSE | 2 +- MANIFEST.in | 4 --- docs/Makefile | 2 +- poetry.lock | 57 ++++------------------------------ pyproject.toml | 1 + tests/data/authz_settings.json | 2 +- tox.ini | 2 +- 7 files changed, 11 insertions(+), 59 deletions(-) delete mode 100644 MANIFEST.in diff --git a/LICENSE b/LICENSE index f193f7d..781617c 100644 --- a/LICENSE +++ b/LICENSE @@ -17,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index acf84af..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include LICENSE -include requirements.txt -include dev-requirements.txt -include docs-requirements.txt diff --git a/docs/Makefile b/docs/Makefile index 28027de..c86fc18 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -17,4 +17,4 @@ help: # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/poetry.lock b/poetry.lock index 06cdcc1..24b870b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -170,7 +170,7 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "coverage" -version = "6.4.1" +version = "6.4.2" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -974,15 +974,15 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "zipp" -version = "3.8.0" +version = "3.8.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [extras] docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd-theme", "readthedocs-sphinx-ext", "m2r2", "sphinx-autoapi"] @@ -1064,49 +1064,7 @@ commonmark = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] -coverage = [ - {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"}, - {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"}, - {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"}, - {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"}, - {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"}, - {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"}, - {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"}, - {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"}, - {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"}, - {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"}, - {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"}, - {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"}, - {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"}, - {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"}, - {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"}, - {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, - {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, -] +coverage = [] decli = [ {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, @@ -1565,7 +1523,4 @@ wrapt = [ {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] -zipp = [ - {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, - {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, -] +zipp = [] diff --git a/pyproject.toml b/pyproject.toml index 0e54eaf..3c32037 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ packages = [ { include = "keycloak", from = "src/" }, { include = "keycloak/**/*.py", from = "src/" }, ] +include = ["LICENSE", "CHANGELOG.md", "CODEOWNERS", "CONTRIBUTING.md"] [tool.poetry.urls] Documentation = "https://python-keycloak.readthedocs.io/en/latest/" diff --git a/tests/data/authz_settings.json b/tests/data/authz_settings.json index e051085..8f11198 100644 --- a/tests/data/authz_settings.json +++ b/tests/data/authz_settings.json @@ -42,4 +42,4 @@ ], "scopes": [], "decisionStrategy": "UNANIMOUS" -} \ No newline at end of file +} diff --git a/tox.ini b/tox.ini index 3fbdd66..219a4d5 100644 --- a/tox.ini +++ b/tox.ini @@ -32,7 +32,7 @@ commands = ./test_keycloak_init.sh "pytest -vv --cov=keycloak --cov-report term-missing {posargs}" [testenv:build] -deps = +deps = poetry setenv = POETRY_VIRTUALENVS_CREATE = false From 7031123c1fa3462290c20fa11c4c80b6732a5bf7 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Wed, 13 Jul 2022 07:31:04 +0000 Subject: [PATCH 18/19] test: finished off openid tests --- src/keycloak/keycloak_openid.py | 4 ++-- tests/conftest.py | 1 - tests/test_keycloak_openid.py | 41 +++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 0a45dc3..e2fcca1 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -524,7 +524,7 @@ class KeycloakOpenID: try: granted = self.uma_permissions(token, permissions) except (KeycloakPostError, KeycloakAuthenticationError) as e: - if e.response_code == 403: + if e.response_code == 403: # pragma: no cover return AuthStatus( is_logged_in=True, is_authorized=False, missing_permissions=needed ) @@ -540,7 +540,7 @@ class KeycloakOpenID: if not scopes: needed.discard(resource) continue - for scope in scopes: + for scope in scopes: # pragma: no cover needed.discard("{}#{}".format(resource, scope)) return AuthStatus( diff --git a/tests/conftest.py b/tests/conftest.py index 632c51b..5f340ae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -192,7 +192,6 @@ def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: Keycloak "roles": [{"id": admin.get_realm_role(role_name="offline_access")["id"]}], }, ) - admin.create_client_authz_resource # Create user username = str(uuid.uuid4()) password = str(uuid.uuid4()) diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 0e94cd3..55c9d44 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -13,6 +13,7 @@ from keycloak.exceptions import ( KeycloakAuthorizationConfigError, KeycloakDeprecationError, KeycloakInvalidTokenError, + KeycloakPostError, KeycloakRPTNotFound, ) from keycloak.keycloak_admin import KeycloakAdmin @@ -349,3 +350,43 @@ def test_get_permissions(oid_with_credentials_authz: tuple[KeycloakOpenID, str, oid.logout(refresh_token=token["refresh_token"]) with pytest.raises(KeycloakInvalidTokenError): oid.get_permissions(token=token["access_token"]) + + +def test_uma_permissions(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): + """Test UMA permissions.""" + oid, username, password = oid_with_credentials_authz + token = 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" + + +def test_has_uma_access( + oid_with_credentials_authz: tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin +): + """Test has UMA access.""" + oid, username, password = oid_with_credentials_authz + token = oid.token(username=username, password=password) + + assert ( + str(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")) + == "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") + + oid.logout(refresh_token=token["refresh_token"]) + assert ( + str(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")) + == "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions=" + + "{'Default Resource'})" + ) From 81cc71c000d9c9c39e354ef1a683221b7150d8c3 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Wed, 13 Jul 2022 07:43:01 +0000 Subject: [PATCH 19/19] test: fix the tests, make them compatible with older python versions --- tests/test_keycloak_openid.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 55c9d44..0324077 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -1,4 +1,5 @@ """Test module for KeycloakOpenID.""" +from typing import Tuple from unittest import mock import pytest @@ -105,11 +106,11 @@ def test_auth_url(env, oid: KeycloakOpenID): res == f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}/realms/{oid.realm_name}" + f"/protocol/openid-connect/auth?client_id={oid.client_id}&response_type=code" - + "&redirect_uri=http://test.test/*" + + "&redirect_uri=http://test.test/*&scope=email&state= " ) -def test_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): +def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): """Test the token method.""" oid, username, password = oid_with_credentials token = oid.token(username=username, password=password) @@ -152,7 +153,7 @@ def test_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): 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.""" # Verify existing user @@ -218,7 +219,7 @@ def test_public_key(oid: KeycloakOpenID): 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.""" oid, username, password = oid_with_credentials_authz @@ -231,7 +232,7 @@ def test_entitlement( oid.entitlement(token=token["access_token"], resource_server_id=resource_server_id) -def test_introspect(oid_with_credentials: tuple[KeycloakOpenID, str, str]): +def test_introspect(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): """Test introspect.""" oid, username, password = oid_with_credentials token = oid.token(username=username, password=password) @@ -245,7 +246,7 @@ def test_introspect(oid_with_credentials: tuple[KeycloakOpenID, str, str]): oid.introspect(token=token["access_token"], token_type_hint="requesting_party_token") -def test_decode_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): +def test_decode_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): """Test decode token.""" oid, username, password = oid_with_credentials token = oid.token(username=username, password=password) @@ -260,7 +261,7 @@ def test_decode_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): ) -def test_load_authorization_config(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): +def test_load_authorization_config(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]): """Test load authorization config.""" oid, username, password = oid_with_credentials_authz @@ -275,7 +276,7 @@ def test_load_authorization_config(oid_with_credentials_authz: tuple[KeycloakOpe ) -def test_get_policies(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): +def test_get_policies(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]): """Test get policies.""" oid, username, password = oid_with_credentials_authz token = oid.token(username=username, password=password) @@ -308,7 +309,7 @@ def test_get_policies(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str oid.get_policies(token=token["access_token"]) -def test_get_permissions(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): +def test_get_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]): """Test get policies.""" oid, username, password = oid_with_credentials_authz token = oid.token(username=username, password=password) @@ -352,7 +353,7 @@ def test_get_permissions(oid_with_credentials_authz: tuple[KeycloakOpenID, str, oid.get_permissions(token=token["access_token"]) -def test_uma_permissions(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): +def test_uma_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]): """Test UMA permissions.""" oid, username, password = oid_with_credentials_authz token = oid.token(username=username, password=password) @@ -362,7 +363,7 @@ def test_uma_permissions(oid_with_credentials_authz: tuple[KeycloakOpenID, str, 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.""" oid, username, password = oid_with_credentials_authz