From fa1ce3bb2b338aafe475a332f432bcfec9628eca Mon Sep 17 00:00:00 2001 From: Mohsin Anees Date: Sat, 22 Jun 2024 13:10:17 +0500 Subject: [PATCH] feat: functions for updating resource permissions and getting associated policies for a permission (#574) * fix: tox.ini config fixed for testenv:check * feat: functions for updating reource permissions and getting associated policies * fix: linting issues resolved * revert: brought back all functions which were mistakenly removed in commit * fix: linting issue resolved to prevent unintended changes in file * fix: comments fixed for docs * fix: async functions created for new functionality * feat: test cases completed for new functionality * chore: tox and deps update --------- Co-authored-by: Richard Nemeth --- poetry.lock | 70 +++++++++---------- src/keycloak/keycloak_admin.py | 122 +++++++++++++++++++++++++++++++++ src/keycloak/urls_patterns.py | 6 ++ tests/test_keycloak_admin.py | 54 +++++++++++++++ tox.ini | 3 - 5 files changed, 217 insertions(+), 38 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5f57a92..a7de063 100644 --- a/poetry.lock +++ b/poetry.lock @@ -632,34 +632,34 @@ test = ["pytest (>=6)"] [[package]] name = "filelock" -version = "3.15.1" +version = "3.15.3" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.15.1-py3-none-any.whl", hash = "sha256:71b3102950e91dfc1bb4209b64be4dc8854f40e5f534428d8684f953ac847fac"}, - {file = "filelock-3.15.1.tar.gz", hash = "sha256:58a2549afdf9e02e10720eaa4d4470f56386d7a6f72edd7d0596337af8ed7ad8"}, + {file = "filelock-3.15.3-py3-none-any.whl", hash = "sha256:0151273e5b5d6cf753a61ec83b3a9b7d8821c39ae9af9d7ecf2f9e2f17404103"}, + {file = "filelock-3.15.3.tar.gz", hash = "sha256:e1199bf5194a2277273dacd50269f0d87d0682088a3c561c15674ea9005d8635"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] name = "flake8" -version = "7.0.0" +version = "7.1.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" files = [ - {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, - {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, + {file = "flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a"}, + {file = "flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"}, ] [package.dependencies] mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.11.0,<2.12.0" +pycodestyle = ">=2.12.0,<2.13.0" pyflakes = ">=3.2.0,<3.3.0" [[package]] @@ -785,22 +785,22 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.1.0" +version = "7.2.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, - {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, + {file = "importlib_metadata-7.2.0-py3-none-any.whl", hash = "sha256:04e4aad329b8b948a5711d394fa8759cb80f009225441b4f2a02bd4d8e5f426c"}, + {file = "importlib_metadata-7.2.0.tar.gz", hash = "sha256:3ff4519071ed42740522d494d04819b666541b9752c43012f85afb2cc220fcc6"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "importlib-resources" @@ -1272,13 +1272,13 @@ wcwidth = "*" [[package]] name = "pycodestyle" -version = "2.11.1" +version = "2.12.0" description = "Python style guide checker" optional = false python-versions = ">=3.8" files = [ - {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, - {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, + {file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"}, + {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"}, ] [[package]] @@ -1336,22 +1336,22 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyproject-api" -version = "1.6.1" +version = "1.7.1" description = "API to interact with the python pyproject.toml based projects" optional = false python-versions = ">=3.8" files = [ - {file = "pyproject_api-1.6.1-py3-none-any.whl", hash = "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675"}, - {file = "pyproject_api-1.6.1.tar.gz", hash = "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538"}, + {file = "pyproject_api-1.7.1-py3-none-any.whl", hash = "sha256:2dc1654062c2b27733d8fd4cdda672b22fe8741ef1dde8e3a998a9547b071eeb"}, + {file = "pyproject_api-1.7.1.tar.gz", hash = "sha256:7ebc6cd10710f89f4cf2a2731710a98abce37ebff19427116ff2174c9236a827"}, ] [package.dependencies] -packaging = ">=23.1" +packaging = ">=24.1" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} [package.extras] -docs = ["furo (>=2023.8.19)", "sphinx (<7.2)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "setuptools (>=68.1.2)", "wheel (>=0.41.2)"] +docs = ["furo (>=2024.5.6)", "sphinx-autodoc-typehints (>=2.2.1)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.2.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=70.1)"] [[package]] name = "pytest" @@ -1657,18 +1657,18 @@ jeepney = ">=0.6" [[package]] name = "setuptools" -version = "70.0.0" +version = "70.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, - {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, + {file = "setuptools-70.1.0-py3-none-any.whl", hash = "sha256:d9b8b771455a97c8a9f3ab3448ebe0b29b5e105f1228bba41028be116985a267"}, + {file = "setuptools-70.1.0.tar.gz", hash = "sha256:01a1e793faa5bd89abc851fa15d0a0db26f160890c7102cd8dce643e886b47f5"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -1740,13 +1740,13 @@ test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] [[package]] name = "sphinx-autoapi" -version = "3.1.1" +version = "3.1.2" description = "Sphinx API documentation generator" optional = false python-versions = ">=3.8" files = [ - {file = "sphinx_autoapi-3.1.1-py2.py3-none-any.whl", hash = "sha256:ff202754c38e119fe60b9336fb3cdbd0b78f8623753fa9151aed42a7052506b3"}, - {file = "sphinx_autoapi-3.1.1.tar.gz", hash = "sha256:b5f6e3c61cd86c0cdb7ee77a9d580c0fd116726c5b29cdcb1f1d5f30a5bca1bd"}, + {file = "sphinx_autoapi-3.1.2-py2.py3-none-any.whl", hash = "sha256:8d672bd2baa8365ac844d3f52c0d3360aa492299131d3dea156a20a26f048d23"}, + {file = "sphinx_autoapi-3.1.2.tar.gz", hash = "sha256:fa5eb188f67ae39e19b2e7d2527c75d064e0f0b9ac7f77a3558ec26ccb731c26"}, ] [package.dependencies] @@ -1981,13 +1981,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] @@ -1998,13 +1998,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.2" +version = "20.26.3" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, - {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, ] [package.dependencies] diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 3e209a3..15874ee 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -4015,6 +4015,43 @@ class KeycloakAdmin: ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[201]) + def update_client_authz_resource_permission(self, payload, client_id, resource_id): + """Update permissions for a given resource. + + Payload example:: + + payload={ + "id": resource_id, + "name": "My Permission Name", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "resources": [some_resource_id], + "scopes": [], + "policies": [some_policy_id], + } + + :param payload: No Document + :type payload: dict + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_clientrepresentation + :type client_id: str + :param resource_id: No Document + :type resource_id: str + :return: Keycloak server response + :rtype: bytes + """ + params_path = { + "realm-name": self.connection.realm_name, + "id": client_id, + "resource-id": resource_id, + } + data_raw = self.connection.raw_put( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCE_PERMISSION.format(**params_path), + data=json.dumps(payload), + ) + 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. @@ -4030,6 +4067,30 @@ class KeycloakAdmin: ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) + def get_client_authz_permission_associated_policies(self, client_id, policy_id): + """Get associated policies for a given client permission. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_clientrepresentation + :type client_id: str + :param policy_id: id in PolicyRepresentation + https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_policyrepresentation + :type policy_id: str + :return: Keycloak server response (RoleRepresentation) + :rtype: list + """ + params_path = { + "realm-name": self.connection.realm_name, + "id": client_id, + "policy-id": policy_id, + } + data_raw = self.connection.raw_get( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_CLIENT_POLICY_ASSOCIATED_POLICIES.format( + **params_path + ) + ) + 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. @@ -8152,6 +8213,43 @@ class KeycloakAdmin: ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[201]) + async def a_update_client_authz_resource_permission(self, payload, client_id, resource_id): + """Update permissions for a given resource asynchronously. + + Payload example:: + + payload={ + "id": resource_id, + "name": "My Permission Name", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "resources": [some_resource_id], + "scopes": [], + "policies": [some_policy_id], + } + + :param payload: No Document + :type payload: dict + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_clientrepresentation + :type client_id: str + :param resource_id: No Document + :type resource_id: str + :return: Keycloak server response + :rtype: bytes + """ + params_path = { + "realm-name": self.connection.realm_name, + "id": client_id, + "resource-id": resource_id, + } + data_raw = await self.connection.a_raw_put( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCE_PERMISSION.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[201]) + async def a_get_client_authz_client_policies(self, client_id): """Get policies for a given client asynchronously. @@ -8167,6 +8265,30 @@ class KeycloakAdmin: ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) + async def a_get_client_authz_permission_associated_policies(self, client_id, policy_id): + """Get associated policies for a given client permission asynchronously. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_clientrepresentation + :type client_id: str + :param policy_id: id in PolicyRepresentation + https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_policyrepresentation + :type policy_id: str + :return: Keycloak server response (RoleRepresentation) + :rtype: list + """ + params_path = { + "realm-name": self.connection.realm_name, + "id": client_id, + "policy-id": policy_id, + } + data_raw = await self.connection.a_raw_get( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_CLIENT_POLICY_ASSOCIATED_POLICIES.format( + **params_path + ) + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) + async def a_create_client_authz_client_policy(self, payload, client_id): """Create a new policy for a given client asynchronously. diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 8829930..a183183 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -129,8 +129,14 @@ URL_ADMIN_CLIENT_AUTHZ_POLICY = URL_ADMIN_CLIENT_AUTHZ + "/policy/{policy-id}" URL_ADMIN_CLIENT_AUTHZ_POLICY_SCOPES = URL_ADMIN_CLIENT_AUTHZ_POLICY + "/scopes" URL_ADMIN_CLIENT_AUTHZ_POLICY_RESOURCES = URL_ADMIN_CLIENT_AUTHZ_POLICY + "/resources" URL_ADMIN_CLIENT_AUTHZ_SCOPE_PERMISSION = URL_ADMIN_CLIENT_AUTHZ + "/permission/scope/{scope-id}" +URL_ADMIN_CLIENT_AUTHZ_RESOURCE_PERMISSION = ( + URL_ADMIN_CLIENT_AUTHZ + "/permission/resource/{resource-id}" +) URL_ADMIN_ADD_CLIENT_AUTHZ_SCOPE_PERMISSION = URL_ADMIN_CLIENT_AUTHZ + "/permission/scope?max=-1" URL_ADMIN_CLIENT_AUTHZ_CLIENT_POLICY = URL_ADMIN_CLIENT_AUTHZ + "/policy/client" +URL_ADMIN_CLIENT_AUTHZ_CLIENT_POLICY_ASSOCIATED_POLICIES = ( + URL_ADMIN_CLIENT_AUTHZ + "/policy/{policy-id}/associatedPolicies" +) URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index c658846..cb88c2b 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1102,6 +1102,8 @@ def test_clients(admin: KeycloakAdmin, realm: str): payload={"name": "test-authz-rb-policy", "roles": [{"id": role_id}]}, ) assert res["name"] == "test-authz-rb-policy", res + role_based_policy_id = res["id"] + role_based_policy_name = res["name"] with pytest.raises(KeycloakPostError) as err: admin.create_client_authz_role_based_policy( @@ -1174,6 +1176,8 @@ def test_clients(admin: KeycloakAdmin, realm: str): assert res, res assert res["name"] == "test-permission-rb" assert res["resources"] == [test_resource_id] + resource_based_permission_id = res["id"] + resource_based_permission_name = res["name"] with pytest.raises(KeycloakPostError) as err: admin.create_client_authz_resource_based_permission( @@ -1188,6 +1192,29 @@ def test_clients(admin: KeycloakAdmin, realm: str): ) == {"msg": "Already exists"} assert len(admin.get_client_authz_permissions(client_id=auth_client_id)) == 2 + # Test associating client policy with resource based permission + res = admin.update_client_authz_resource_permission( + client_id=auth_client_id, + resource_id=resource_based_permission_id, + payload={ + "id": resource_based_permission_id, + "name": resource_based_permission_name, + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "resources": [test_resource_id], + "scopes": [], + "policies": [role_based_policy_id], + }, + ) + + # Test getting associated policies for a permission + associated_policies = admin.get_client_authz_permission_associated_policies( + client_id=auth_client_id, policy_id=resource_based_permission_id + ) + assert len(associated_policies) == 1 + assert associated_policies[0]["name"].startswith(role_based_policy_name) + # Test authz scopes res = admin.get_client_authz_scopes(client_id=auth_client_id) assert len(res) == 0, res @@ -4102,6 +4129,8 @@ async def test_a_clients(admin: KeycloakAdmin, realm: str): skip_exists=True, ) == {"msg": "Already exists"} assert len(await admin.a_get_client_authz_policies(client_id=auth_client_id)) == 2 + role_based_policy_id = res["id"] + role_based_policy_name = res["name"] res = await admin.a_create_client_authz_role_based_policy( client_id=auth_client_id, @@ -4161,6 +4190,8 @@ async def test_a_clients(admin: KeycloakAdmin, realm: str): assert res, res assert res["name"] == "test-permission-rb" assert res["resources"] == [test_resource_id] + resource_based_permission_id = res["id"] + resource_based_permission_name = res["name"] with pytest.raises(KeycloakPostError) as err: await admin.a_create_client_authz_resource_based_permission( @@ -4175,6 +4206,29 @@ async def test_a_clients(admin: KeycloakAdmin, realm: str): ) == {"msg": "Already exists"} assert len(await admin.a_get_client_authz_permissions(client_id=auth_client_id)) == 2 + # Test associating client policy with resource based permission + res = await admin.a_update_client_authz_resource_permission( + client_id=auth_client_id, + resource_id=resource_based_permission_id, + payload={ + "id": resource_based_permission_id, + "name": resource_based_permission_name, + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "resources": [test_resource_id], + "scopes": [], + "policies": [role_based_policy_id], + }, + ) + + # Test getting associated policies for a permission + associated_policies = await admin.a_get_client_authz_permission_associated_policies( + client_id=auth_client_id, policy_id=resource_based_permission_id + ) + assert len(associated_policies) == 1 + assert associated_policies[0]["name"].startswith(role_based_policy_name) + # Test authz scopes res = await admin.a_get_client_authz_scopes(client_id=auth_client_id) assert len(res) == 0, res diff --git a/tox.ini b/tox.ini index ce43b2b..01db804 100644 --- a/tox.ini +++ b/tox.ini @@ -14,19 +14,16 @@ commands = isort -c --df src/keycloak tests docs flake8 src/keycloak tests docs codespell src tests docs -allowlist_externals = black, poetry [testenv:apply-check] commands = black -C src/keycloak tests docs black src/keycloak tests docs isort src/keycloak tests docs -allowlist_externals = black, poetry, isort [testenv:docs] commands = sphinx-build -T -E -W -b html -d _build/doctrees -D language=en ./docs/source _build/html -allowlist_externals = sphinx-build, poetry [testenv:tests] setenv = file|tox.env