From cc82e6a8749da1188a8ce57fa2bd9d4e899233b6 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 11:51:53 +0200 Subject: [PATCH 1/9] feat: initial setup of CICD and linting --- .circleci/config.yml | 37 - .github/workflows/bump.yaml | 32 + .github/workflows/daily.yaml | 27 + .github/workflows/lint.yaml | 90 ++ .github/workflows/publish.yaml | 33 + .gitignore | 3 +- .pre-commit-config.yaml | 16 + .readthedocs.yaml | 10 + .releaserc.json | 8 + CHANGELOG.md | 31 +- CODEOWNERS | 1 + CONTRIBUTING.md | 86 ++ MANIFEST.in | 3 + dev-requirements.txt | 5 + docs-requirements.txt | 9 + docs/source/conf.py | 70 +- keycloak/_version.py | 1 + keycloak/authorization/__init__.py | 53 +- keycloak/authorization/policy.py | 5 +- keycloak/connection.py | 102 ++- keycloak/exceptions.py | 26 +- keycloak/keycloak_admin.py | 626 ++++++++----- keycloak/keycloak_openid.py | 140 +-- keycloak/tests/test_connection.py | 191 ---- keycloak/urls_patterns.py | 59 +- pyproject.toml | 6 + requirements.txt | 6 +- setup.py | 65 +- test_keycloak_init.sh | 35 + {keycloak/tests => tests}/__init__.py | 0 tests/conftest.py | 61 ++ tests/test_keycloak_admin.py | 1201 +++++++++++++++++++++++++ tests/test_urls_patterns.py | 26 + tox.env | 4 + tox.ini | 48 + 35 files changed, 2416 insertions(+), 700 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/bump.yaml create mode 100644 .github/workflows/daily.yaml create mode 100644 .github/workflows/lint.yaml create mode 100644 .github/workflows/publish.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yaml create mode 100644 .releaserc.json create mode 100644 CODEOWNERS create mode 100644 CONTRIBUTING.md create mode 100644 dev-requirements.txt create mode 100644 docs-requirements.txt create mode 100644 keycloak/_version.py delete mode 100644 keycloak/tests/test_connection.py create mode 100644 pyproject.toml create mode 100755 test_keycloak_init.sh rename {keycloak/tests => tests}/__init__.py (100%) create mode 100644 tests/conftest.py create mode 100644 tests/test_keycloak_admin.py create mode 100644 tests/test_urls_patterns.py create mode 100644 tox.env create mode 100644 tox.ini diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index e2ab2c3..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,37 +0,0 @@ -version: 2 -jobs: - build: - docker: - - image: circleci/python:3.6.1 - - working_directory: ~/repo - - steps: - - checkout - - restore_cache: - keys: - - v1-dependencies-{{ checksum "requirements.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - - run: - name: install dependencies - command: | - python3 -m venv venv - . venv/bin/activate - pip install -r requirements.txt - - - save_cache: - paths: - - ./venv - key: v1-dependencies-{{ checksum "requirements.txt" }} - - - run: - name: run tests - command: | - . venv/bin/activate - python3 -m unittest discover - - - store_artifacts: - path: test-reports - destination: test-reports \ No newline at end of file diff --git a/.github/workflows/bump.yaml b/.github/workflows/bump.yaml new file mode 100644 index 0000000..e562346 --- /dev/null +++ b/.github/workflows/bump.yaml @@ -0,0 +1,32 @@ +name: Bump version + +on: + workflow_run: + workflows: [ "Lint" ] + branches: [ master ] + types: + - completed + +jobs: + tag-version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + token: ${{ secrets.PAT_TOKEN }} + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: determine-version + run: | + VERSION=$(npx semantic-release --branches master --dry-run | { grep -i 'the next release version is' || test $? = 1; } | sed -E 's/.* ([[:digit:].]+)$/\1/') + echo "VERSION=$VERSION" >> $GITHUB_ENV + id: version + - uses: rickstaa/action-create-tag@v1 + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} + with: + tag: v${{ env.VERSION }} + message: "Releasing v${{ env.VERSION }}" + github_token: ${{ secrets.PAT_TOKEN }} diff --git a/.github/workflows/daily.yaml b/.github/workflows/daily.yaml new file mode 100644 index 0000000..7ddc622 --- /dev/null +++ b/.github/workflows/daily.yaml @@ -0,0 +1,27 @@ +name: Daily check + +on: + schedule: + - cron: '0 4 * * *' + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - uses: docker-practice/actions-setup-docker@master + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Run tests + run: | + tox -e tests diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..2cade19 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,90 @@ +name: Lint + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + check-commits: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: webiny/action-conventional-commits@v1.0.3 + + check-linting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Check linting, formatting + run: | + tox -e check + + check-docs: + runs-on: ubuntu-latest + needs: + - check-commits + - check-linting + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Check documentation build + run: | + tox -e docs + + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10"] + needs: + - check-commits + - check-linting + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - uses: docker-practice/actions-setup-docker@master + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Run tests + run: | + tox -e tests + + build: + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Run build + run: | + tox -e build diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..9519829 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,33 @@ +name: Publish + +on: + push: + tags: + - 'v*' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox wheel twine + - name: Apply the tag version + run: | + version=${{ github.ref_name }} + sed -i 's/__version__ = .*/__version__ = "'${version:1}'"/' keycloak/_version.py + - name: Run build + run: | + tox -e build + - name: Publish to PyPi + env: + TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} + TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} + run: | + twine upload -u $TWINE_USERNAME -p $TWINE_PASSWORD dist/* diff --git a/.gitignore b/.gitignore index 7ea9902..4c8d46d 100644 --- a/.gitignore +++ b/.gitignore @@ -103,4 +103,5 @@ ENV/ .idea/ main.py main2.py -s3air-authz-config.json \ No newline at end of file +s3air-authz-config.json +.vscode \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..806a12c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - repo: https://github.com/compilerla/conventional-pre-commit + rev: v1.2.0 + hooks: + - id: conventional-pre-commit + stages: [ commit-msg ] + args: [ ] # optional: list of Conventional Commits types to allow diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..7aa6ce5 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,10 @@ +version: 2 + +build: + os: "ubuntu-20.04" + tools: + python: "3.10" + +python: + install: + - requirements: docs-requirements.txt diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..c89e61e --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,8 @@ +{ + "plugins": ["@semantic-release/commit-analyzer"], + "verifyConditions": false, + "npmPublish": false, + "publish": false, + "fail": false, + "success": false +} diff --git a/CHANGELOG.md b/CHANGELOG.md index c8891db..4492c3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,45 +1,44 @@ -Changelog -============ +# Changelog All notable changes to this project will be documented in this file. ## [0.5.0] - 2017-08-21 -* Basic functions for Keycloak API (well_know, token, userinfo, logout, certs, -entitlement, instropect) +- Basic functions for Keycloak API (well_know, token, userinfo, logout, certs, + entitlement, instropect) ## [0.6.0] - 2017-08-23 -* Added load authorization settings +- Added load authorization settings ## [0.7.0] - 2017-08-23 -* Added polices +- Added polices ## [0.8.0] - 2017-08-23 -* Added permissions +- Added permissions ## [0.9.0] - 2017-09-05 -* Added functions for Admin Keycloak API +- Added functions for Admin Keycloak API ## [0.10.0] - 2017-10-23 -* Updated libraries versions -* Updated Docs +- Updated libraries versions +- Updated Docs ## [0.11.0] - 2017-12-12 -* Changed Instropect RPT +- Changed Instropect RPT ## [0.12.0] - 2018-01-25 -* Add groups functions -* Add Admin Tasks for user and client role management -* Function to trigger user sync from provider +- Add groups functions +- Add Admin Tasks for user and client role management +- Function to trigger user sync from provider ## [0.12.1] - 2018-08-04 -* Add get_idps -* Rework group functions +- Add get_idps +- Rework group functions diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..853ebe5 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @ryshoooo @marcospereirampj diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..683f7ee --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,86 @@ +# Contributing + +Welcome to the Python Keycloak contributing guidelines. We are all more than happy to receive +any contributions to the repository and want to thank you in advance for your contributions! +This document outlines the process and the guidelines on how contributions work for this repository. + +## Setting up the dev environment + +The development environment is mainly up to the developer. Our recommendations are to create a python +virtual environment and install the necessary requirements. Example + +```sh +python -m venv venv +source venv/bin/activate +python -m pip install -U pip +python -m pip install -r requirements.txt +python -m pip install -r dev-requirements.txt +``` + +## Running checks and tests + +We're utilizing `tox` for most of the testing workflows. However we also have an external dependency on `docker`. +We're using docker to spin up a local keycloak instance which we run our test cases against. This is to avoid +a lot of unnecessary mocking and yet have immediate feedback from the actual Keycloak instance. All of the setup +is done for you with the tox environments, all you need is to have both tox and docker installed +(`tox` is included in the `dev-requirements.txt`). + +To run the unit tests, simply run + +```sh +tox -e tests +``` + +The project is also adhering to strict linting (flake8) and formatting (black + isort). You can always check that +your code changes adhere to the format by running + +```sh +tox -e check +``` + +If the check fails, you'll see an error message specifying what went wrong. To simplify things, you can also run + +```sh +tox -e apply-check +``` + +which will apply isort and black formatting for you in the repository. The flake8 problems however need to be resolved +manually by the developer. + +Additionally we require that the documentation pages are built without warnings. This check is also run via tox, using +the command + +```sh +tox -e docs +``` + +The check is also run in the CICD pipelines. We require that the documentation pages built from the code docstrings +do not create visually "bad" pages. + +## Conventional commits + +Commits to this project must adhere to the [Conventional Commits +specification](https://www.conventionalcommits.org/en/v1.0.0/) that will allow +us to automate version bumps and changelog entry creation. + +After cloning this repository, you must install the pre-commit hook for +conventional commits (this is included in the `dev-requirements.txt`) + +```sh +python3 -m venv .venv +source .venv/bin/activate +python3 -m pip install pre-commit +pre-commit install --install-hooks -t pre-commit -t pre-push -t commit-msg +``` + +## How to contribute + +1. Fork this repository, develop and test your changes +2. Make sure that your changes do not decrease the test coverage +3. Make sure you're commits follow the conventional commits +4. Submit a pull request + +## How to release + +The CICD pipelines are set up for the repository. When a PR is merged, a new version of the library +will be automatically deployed to the PyPi server, meaning you'll be able to see your changes immediately. diff --git a/MANIFEST.in b/MANIFEST.in index 1aba38f..acf84af 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,4 @@ include LICENSE +include requirements.txt +include dev-requirements.txt +include docs-requirements.txt diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..d2f6981 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,5 @@ +tox +pytest +pytest-cov +wheel +pre-commit diff --git a/docs-requirements.txt b/docs-requirements.txt new file mode 100644 index 0000000..343bf89 --- /dev/null +++ b/docs-requirements.txt @@ -0,0 +1,9 @@ +mock +alabaster +commonmark +recommonmark +sphinx +sphinx-rtd-theme +readthedocs-sphinx-ext +m2r2 +sphinx-autoapi diff --git a/docs/source/conf.py b/docs/source/conf.py index 166a8f4..23e9bbf 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -32,37 +32,37 @@ import sphinx_rtd_theme # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.viewcode', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'python-keycloak' -copyright = '2017, Marcos Pereira' -author = 'Marcos Pereira' +project = "python-keycloak" +copyright = "2017, Marcos Pereira" +author = "Marcos Pereira" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.27.1' +version = "0.27.1" # The full version, including alpha/beta/rc tags. -release = '0.27.1' +release = "0.27.1" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -74,13 +74,13 @@ language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] add_function_parentheses = False add_module_names = True # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True @@ -91,7 +91,7 @@ todo_include_todos = True # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Theme options are theme-specific and customize the look and feel of a theme @@ -103,7 +103,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] html_use_smartypants = False @@ -116,7 +116,7 @@ html_show_copyright = True # # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -#html_sidebars = { +# html_sidebars = { # '**': [ # 'about.html', # 'navigation.html', @@ -124,13 +124,13 @@ html_show_copyright = True # 'searchbox.html', # 'donate.html', # ] -#} +# } # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'python-keycloakdoc' +htmlhelp_basename = "python-keycloakdoc" # -- Options for LaTeX output --------------------------------------------- @@ -139,15 +139,12 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -157,8 +154,13 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'python-keycloak.tex', 'python-keycloak Documentation', - 'Marcos Pereira', 'manual'), + ( + master_doc, + "python-keycloak.tex", + "python-keycloak Documentation", + "Marcos Pereira", + "manual", + ) ] @@ -166,10 +168,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'python-keycloak', 'python-keycloak Documentation', - [author], 1) -] +man_pages = [(master_doc, "python-keycloak", "python-keycloak Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -178,10 +177,13 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'python-keycloak', 'python-keycloak Documentation', - author, 'python-keycloak', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "python-keycloak", + "python-keycloak Documentation", + author, + "python-keycloak", + "One line description of project.", + "Miscellaneous", + ) ] - - - diff --git a/keycloak/_version.py b/keycloak/_version.py new file mode 100644 index 0000000..6c8e6b9 --- /dev/null +++ b/keycloak/_version.py @@ -0,0 +1 @@ +__version__ = "0.0.0" diff --git a/keycloak/authorization/__init__.py b/keycloak/authorization/__init__.py index 219687b..dad1078 100644 --- a/keycloak/authorization/__init__.py +++ b/keycloak/authorization/__init__.py @@ -55,39 +55,44 @@ class Authorization: :param data: keycloak authorization data (dict) :return: """ - for pol in data['policies']: - if pol['type'] == 'role': - policy = Policy(name=pol['name'], - type=pol['type'], - logic=pol['logic'], - decision_strategy=pol['decisionStrategy']) - - config_roles = json.loads(pol['config']['roles']) + for pol in data["policies"]: + if pol["type"] == "role": + policy = Policy( + name=pol["name"], + type=pol["type"], + logic=pol["logic"], + decision_strategy=pol["decisionStrategy"], + ) + + config_roles = json.loads(pol["config"]["roles"]) for role in config_roles: - policy.add_role(Role(name=role['id'], - required=role['required'])) + policy.add_role(Role(name=role["id"], required=role["required"])) self.policies[policy.name] = policy - if pol['type'] == 'scope': - permission = Permission(name=pol['name'], - type=pol['type'], - logic=pol['logic'], - decision_strategy=pol['decisionStrategy']) + if pol["type"] == "scope": + permission = Permission( + name=pol["name"], + type=pol["type"], + logic=pol["logic"], + decision_strategy=pol["decisionStrategy"], + ) - permission.scopes = ast.literal_eval(pol['config']['scopes']) + permission.scopes = ast.literal_eval(pol["config"]["scopes"]) - for policy_name in ast.literal_eval(pol['config']['applyPolicies']): + for policy_name in ast.literal_eval(pol["config"]["applyPolicies"]): self.policies[policy_name].add_permission(permission) - if pol['type'] == 'resource': - permission = Permission(name=pol['name'], - type=pol['type'], - logic=pol['logic'], - decision_strategy=pol['decisionStrategy']) + if pol["type"] == "resource": + permission = Permission( + name=pol["name"], + type=pol["type"], + logic=pol["logic"], + decision_strategy=pol["decisionStrategy"], + ) - permission.resources = ast.literal_eval(pol['config'].get('resources', "[]")) + permission.resources = ast.literal_eval(pol["config"].get("resources", "[]")) - for policy_name in ast.literal_eval(pol['config']['applyPolicies']): + for policy_name in ast.literal_eval(pol["config"]["applyPolicies"]): if self.policies.get(policy_name) is not None: self.policies[policy_name].add_permission(permission) diff --git a/keycloak/authorization/policy.py b/keycloak/authorization/policy.py index 9f688f7..4fbe913 100644 --- a/keycloak/authorization/policy.py +++ b/keycloak/authorization/policy.py @@ -98,9 +98,10 @@ class Policy: :param role: keycloak role. :return: """ - if self.type != 'role': + if self.type != "role": raise KeycloakAuthorizationConfigError( - "Can't add role. Policy type is different of role") + "Can't add role. Policy type is different of role" + ) self._roles.append(role) def add_permission(self, permission): diff --git a/keycloak/connection.py b/keycloak/connection.py index bdecfce..8ef45b1 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -29,11 +29,11 @@ except ImportError: import requests from requests.adapters import HTTPAdapter -from .exceptions import (KeycloakConnectionError) +from .exceptions import KeycloakConnectionError class ConnectionManager(object): - """ Represents a simple server connection. + """Represents a simple server connection. Args: base_url (str): The server URL. headers (dict): The header parameters of the requests to the server. @@ -52,15 +52,15 @@ class ConnectionManager(object): # retry once to reset connection with Keycloak after tomcat's ConnectionTimeout # see https://github.com/marcospereirampj/python-keycloak/issues/36 - for protocol in ('https://', 'http://'): + for protocol in ("https://", "http://"): adapter = HTTPAdapter(max_retries=1) # adds POST to retry whitelist allowed_methods = set(adapter.max_retries.allowed_methods) - allowed_methods.add('POST') + allowed_methods.add("POST") adapter.max_retries.allowed_methods = frozenset(allowed_methods) self._s.mount(protocol, adapter) - + if proxies: self._s.proxies.update(proxies) @@ -69,7 +69,7 @@ class ConnectionManager(object): @property def base_url(self): - """ Return base url in use for requests to the server. """ + """Return base url in use for requests to the server.""" return self._base_url @base_url.setter @@ -79,7 +79,7 @@ class ConnectionManager(object): @property def timeout(self): - """ Return timeout in use for request to the server. """ + """Return timeout in use for request to the server.""" return self._timeout @timeout.setter @@ -89,7 +89,7 @@ class ConnectionManager(object): @property def verify(self): - """ Return verify in use for request to the server. """ + """Return verify in use for request to the server.""" return self._verify @verify.setter @@ -99,7 +99,7 @@ class ConnectionManager(object): @property def headers(self): - """ Return header request to the server. """ + """Return header request to the server.""" return self._headers @headers.setter @@ -108,7 +108,7 @@ class ConnectionManager(object): self._headers = value def param_headers(self, key): - """ Return a specific header parameter. + """Return a specific header parameter. :arg key (str): Header parameters key. :return: @@ -117,11 +117,11 @@ class ConnectionManager(object): return self.headers.get(key) def clean_headers(self): - """ Clear header parameters. """ + """Clear header parameters.""" self.headers = {} def exist_param_headers(self, key): - """ Check if the parameter exists in the header. + """Check if the parameter exists in the header. :arg key (str): Header parameters key. :return: @@ -130,7 +130,7 @@ class ConnectionManager(object): return self.param_headers(key) is not None def add_param_headers(self, key, value): - """ Add a single parameter inside the header. + """Add a single parameter inside the header. :arg key (str): Header parameters key. value (str): Value to be added. @@ -138,14 +138,14 @@ class ConnectionManager(object): self.headers[key] = value def del_param_headers(self, key): - """ Remove a specific parameter. + """Remove a specific parameter. :arg key (str): Key of the header parameters. """ self.headers.pop(key, None) def raw_get(self, path, **kwargs): - """ Submit get request to the path. + """Submit get request to the path. :arg path (str): Path for request. :return @@ -155,17 +155,18 @@ class ConnectionManager(object): """ try: - return self._s.get(urljoin(self.base_url, path), - params=kwargs, - headers=self.headers, - timeout=self.timeout, - verify=self.verify) + return self._s.get( + urljoin(self.base_url, path), + params=kwargs, + headers=self.headers, + timeout=self.timeout, + verify=self.verify, + ) except Exception as e: - raise KeycloakConnectionError( - "Can't connect to server (%s)" % e) + raise KeycloakConnectionError("Can't connect to server (%s)" % e) def raw_post(self, path, data, **kwargs): - """ Submit post request to the path. + """Submit post request to the path. :arg path (str): Path for request. data (dict): Payload for request. @@ -175,18 +176,19 @@ class ConnectionManager(object): HttpError: Can't connect to server. """ try: - return self._s.post(urljoin(self.base_url, path), - params=kwargs, - data=data, - headers=self.headers, - timeout=self.timeout, - verify=self.verify) + return self._s.post( + urljoin(self.base_url, path), + params=kwargs, + data=data, + headers=self.headers, + timeout=self.timeout, + verify=self.verify, + ) except Exception as e: - raise KeycloakConnectionError( - "Can't connect to server (%s)" % e) + raise KeycloakConnectionError("Can't connect to server (%s)" % e) def raw_put(self, path, data, **kwargs): - """ Submit put request to the path. + """Submit put request to the path. :arg path (str): Path for request. data (dict): Payload for request. @@ -196,18 +198,19 @@ class ConnectionManager(object): HttpError: Can't connect to server. """ try: - return self._s.put(urljoin(self.base_url, path), - params=kwargs, - data=data, - headers=self.headers, - timeout=self.timeout, - verify=self.verify) + return self._s.put( + urljoin(self.base_url, path), + params=kwargs, + data=data, + headers=self.headers, + timeout=self.timeout, + verify=self.verify, + ) except Exception as e: - raise KeycloakConnectionError( - "Can't connect to server (%s)" % e) + raise KeycloakConnectionError("Can't connect to server (%s)" % e) def raw_delete(self, path, data={}, **kwargs): - """ Submit delete request to the path. + """Submit delete request to the path. :arg path (str): Path for request. @@ -218,12 +221,13 @@ class ConnectionManager(object): HttpError: Can't connect to server. """ try: - return self._s.delete(urljoin(self.base_url, path), - params=kwargs, - data=data, - headers=self.headers, - timeout=self.timeout, - verify=self.verify) + return self._s.delete( + urljoin(self.base_url, path), + params=kwargs, + data=data, + headers=self.headers, + timeout=self.timeout, + verify=self.verify, + ) except Exception as e: - raise KeycloakConnectionError( - "Can't connect to server (%s)" % e) + raise KeycloakConnectionError("Can't connect to server (%s)" % e) diff --git a/keycloak/exceptions.py b/keycloak/exceptions.py index 67da62a..a9c1b2b 100644 --- a/keycloak/exceptions.py +++ b/keycloak/exceptions.py @@ -25,8 +25,7 @@ import requests class KeycloakError(Exception): - def __init__(self, error_message="", response_code=None, - response_body=None): + def __init__(self, error_message="", response_code=None, response_body=None): Exception.__init__(self, error_message) @@ -56,10 +55,23 @@ class KeycloakOperationError(KeycloakError): class KeycloakDeprecationError(KeycloakError): pass + class KeycloakGetError(KeycloakOperationError): pass +class KeycloakPostError(KeycloakOperationError): + pass + + +class KeycloakPutError(KeycloakOperationError): + pass + + +class KeycloakDeleteError(KeycloakOperationError): + pass + + class KeycloakSecretNotFound(KeycloakOperationError): pass @@ -90,10 +102,10 @@ def raise_error_from_response(response, error, expected_codes=None, skip_exists= return response.content if skip_exists and response.status_code == 409: - return {"Already exists"} + return {"msg": "Already exists"} try: - message = response.json()['message'] + message = response.json()["message"] except (KeyError, ValueError): message = response.content @@ -103,6 +115,6 @@ def raise_error_from_response(response, error, expected_codes=None, skip_exists= if response.status_code == 401: error = KeycloakAuthenticationError - raise error(error_message=message, - response_code=response.status_code, - response_body=response.content) + raise error( + error_message=message, response_code=response.status_code, response_body=response.content + ) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 41ecab8..41f4567 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -29,33 +29,91 @@ from builtins import isinstance from typing import Iterable from .connection import ConnectionManager -from .exceptions import raise_error_from_response, KeycloakGetError +from .exceptions import KeycloakGetError, raise_error_from_response from .keycloak_openid import KeycloakOpenID - -from .urls_patterns import URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS, URL_ADMIN_CLIENT_AUTHZ_POLICIES, \ - URL_ADMIN_CLIENT_AUTHZ_SCOPES, URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \ - URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY, URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION, \ - URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_GROUPS_REALM_ROLES, \ - URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ - URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES, \ - URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, URL_ADMIN_USER_GROUP, URL_ADMIN_REALM_ROLES, URL_ADMIN_GROUP_CHILD, \ - URL_ADMIN_USER_CONSENTS, URL_ADMIN_SEND_VERIFY_EMAIL, URL_ADMIN_CLIENT, URL_ADMIN_USER, URL_ADMIN_CLIENT_ROLE, \ - URL_ADMIN_USER_GROUPS, URL_ADMIN_CLIENTS, URL_ADMIN_FLOWS_EXECUTIONS, URL_ADMIN_GROUPS, URL_ADMIN_USER_CLIENT_ROLES, \ - URL_ADMIN_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ - URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, URL_ADMIN_IDP, \ - URL_ADMIN_IDP_MAPPERS, URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ - URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_USER_REALM_ROLES_AVAILABLE, URL_ADMIN_USER_REALM_ROLES_COMPOSITE, \ - URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ - URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_ROLE_MEMBERS, \ - URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \ - URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ - URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG, \ - URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS, \ - URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT, URL_ADMIN_FLOWS_EXECUTION, \ - URL_ADMIN_FLOW, URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES, URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE, \ - URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES, URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE, \ - URL_ADMIN_USER_CREDENTIALS, URL_ADMIN_USER_CREDENTIAL, URL_ADMIN_CLIENT_PROTOCOL_MAPPERS +from .urls_patterns import ( + URL_ADMIN_AUTHENTICATOR_CONFIG, + URL_ADMIN_CLIENT, + URL_ADMIN_CLIENT_ALL_SESSIONS, + URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS, + URL_ADMIN_CLIENT_AUTHZ_POLICIES, + URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION, + URL_ADMIN_CLIENT_AUTHZ_RESOURCES, + URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY, + URL_ADMIN_CLIENT_AUTHZ_SCOPES, + URL_ADMIN_CLIENT_AUTHZ_SETTINGS, + URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, + URL_ADMIN_CLIENT_PROTOCOL_MAPPER, + URL_ADMIN_CLIENT_PROTOCOL_MAPPERS, + URL_ADMIN_CLIENT_ROLE, + URL_ADMIN_CLIENT_ROLE_MEMBERS, + URL_ADMIN_CLIENT_ROLES, + URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, + URL_ADMIN_CLIENT_SCOPE, + URL_ADMIN_CLIENT_SCOPES, + URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, + URL_ADMIN_CLIENT_SCOPES_MAPPERS, + URL_ADMIN_CLIENT_SECRETS, + URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, + URL_ADMIN_CLIENTS, + URL_ADMIN_COMPONENT, + URL_ADMIN_COMPONENTS, + URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE, + URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES, + URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE, + URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES, + URL_ADMIN_DELETE_USER_ROLE, + URL_ADMIN_EVENTS, + URL_ADMIN_FLOW, + URL_ADMIN_FLOWS, + URL_ADMIN_FLOWS_ALIAS, + URL_ADMIN_FLOWS_COPY, + URL_ADMIN_FLOWS_EXECUTION, + URL_ADMIN_FLOWS_EXECUTIONS, + URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION, + URL_ADMIN_FLOWS_EXECUTIONS_FLOW, + URL_ADMIN_GET_SESSIONS, + URL_ADMIN_GROUP, + URL_ADMIN_GROUP_CHILD, + URL_ADMIN_GROUP_MEMBERS, + URL_ADMIN_GROUP_PERMISSIONS, + URL_ADMIN_GROUPS, + URL_ADMIN_GROUPS_CLIENT_ROLES, + URL_ADMIN_GROUPS_REALM_ROLES, + URL_ADMIN_IDP, + URL_ADMIN_IDP_MAPPERS, + URL_ADMIN_IDPS, + URL_ADMIN_KEYS, + URL_ADMIN_REALM, + URL_ADMIN_REALM_EXPORT, + URL_ADMIN_REALM_ROLES, + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, + URL_ADMIN_REALM_ROLES_MEMBERS, + URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, + URL_ADMIN_REALMS, + URL_ADMIN_RESET_PASSWORD, + URL_ADMIN_SEND_UPDATE_ACCOUNT, + URL_ADMIN_SEND_VERIFY_EMAIL, + URL_ADMIN_SERVER_INFO, + URL_ADMIN_USER, + URL_ADMIN_USER_CLIENT_ROLES, + URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, + URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, + URL_ADMIN_USER_CONSENTS, + URL_ADMIN_USER_CREDENTIAL, + URL_ADMIN_USER_CREDENTIALS, + URL_ADMIN_USER_FEDERATED_IDENTITIES, + URL_ADMIN_USER_FEDERATED_IDENTITY, + URL_ADMIN_USER_GROUP, + URL_ADMIN_USER_GROUPS, + URL_ADMIN_USER_LOGOUT, + URL_ADMIN_USER_REALM_ROLES, + URL_ADMIN_USER_REALM_ROLES_AVAILABLE, + URL_ADMIN_USER_REALM_ROLES_COMPOSITE, + URL_ADMIN_USER_STORAGE, + URL_ADMIN_USERS, + URL_ADMIN_USERS_COUNT, +) class KeycloakAdmin: @@ -75,8 +133,19 @@ class KeycloakAdmin: _custom_headers = None _user_realm_name = None - def __init__(self, server_url, username=None, password=None, realm_name='master', client_id='admin-cli', verify=True, - client_secret_key=None, custom_headers=None, user_realm_name=None, auto_refresh_token=None): + def __init__( + self, + server_url, + username=None, + password=None, + realm_name="master", + client_id="admin-cli", + verify=True, + client_secret_key=None, + custom_headers=None, + user_realm_name=None, + auto_refresh_token=None, + ): """ :param server_url: Keycloak server url @@ -198,40 +267,46 @@ class KeycloakAdmin: @auto_refresh_token.setter def auto_refresh_token(self, value): - allowed_methods = {'get', 'post', 'put', 'delete'} + allowed_methods = {"get", "post", "put", "delete"} if not isinstance(value, Iterable): - raise TypeError('Expected a list of strings among {allowed}'.format(allowed=allowed_methods)) + raise TypeError( + "Expected a list of strings among {allowed}".format(allowed=allowed_methods) + ) if not all(method in allowed_methods for method in value): - raise TypeError('Unexpected method in auto_refresh_token, accepted methods are {allowed}'.format(allowed=allowed_methods)) + raise TypeError( + "Unexpected method in auto_refresh_token, accepted methods are {allowed}".format( + allowed=allowed_methods + ) + ) self._auto_refresh_token = value def __fetch_all(self, url, query=None): - '''Wrapper function to paginate GET requests + """Wrapper function to paginate GET requests :param url: The url on which the query is executed :param query: Existing query parameters (optional) :return: Combined results of paginated queries - ''' + """ results = [] # initalize query if it was called with None if not query: query = {} page = 0 - query['max'] = self.PAGE_SIZE + query["max"] = self.PAGE_SIZE # fetch until we can while True: - query['first'] = page*self.PAGE_SIZE + query["first"] = page * self.PAGE_SIZE partial_results = raise_error_from_response( - self.raw_get(url, **query), - KeycloakGetError) + self.raw_get(url, **query), KeycloakGetError + ) if not partial_results: break results.extend(partial_results) - if len(partial_results) < query['max']: + if len(partial_results) < query["max"]: break page += 1 return results @@ -239,9 +314,7 @@ class KeycloakAdmin: def __fetch_paginated(self, url, query=None): query = query or {} - return raise_error_from_response( - self.raw_get(url, **query), - KeycloakGetError) + return raise_error_from_response(self.raw_get(url, **query), KeycloakGetError) def import_realm(self, payload): """ @@ -255,8 +328,7 @@ class KeycloakAdmin: :return: RealmRepresentation """ - data_raw = self.raw_post(URL_ADMIN_REALMS, - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def export_realm(self, export_clients=False, export_groups_and_role=False): @@ -271,7 +343,11 @@ class KeycloakAdmin: :return: realm configurations JSON """ - params_path = {"realm-name": self.realm_name, "export-clients": export_clients, "export-groups-and-roles": export_groups_and_role } + params_path = { + "realm-name": self.realm_name, + "export-clients": export_clients, + "export-groups-and-roles": export_groups_and_role, + } data_raw = self.raw_post(URL_ADMIN_REALM_EXPORT.format(**params_path), data="") return raise_error_from_response(data_raw, KeycloakGetError) @@ -296,9 +372,10 @@ class KeycloakAdmin: :return: Keycloak server response (RealmRepresentation) """ - data_raw = self.raw_post(URL_ADMIN_REALMS, - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def update_realm(self, realm_name, payload): """ @@ -314,8 +391,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": realm_name} - data_raw = self.raw_put(URL_ADMIN_REALM.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put(URL_ADMIN_REALM.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_realm(self, realm_name): @@ -359,8 +435,7 @@ class KeycloakAdmin: :param: payload: IdentityProviderRepresentation """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_IDPS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_IDPS.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def add_mapper_to_idp(self, idp_alias, payload): @@ -374,8 +449,9 @@ class KeycloakAdmin: :param: payload: IdentityProviderMapperRepresentation """ params_path = {"realm-name": self.realm_name, "idp-alias": idp_alias} - data_raw = self.raw_post(URL_ADMIN_IDP_MAPPERS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_IDP_MAPPERS.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def get_idps(self): @@ -416,16 +492,15 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name} if exist_ok: - exists = self.get_user_id(username=payload['username']) + exists = self.get_user_id(username=payload["username"]) if exists is not None: return str(exists) - data_raw = self.raw_post(URL_ADMIN_USERS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_USERS.format(**params_path), data=json.dumps(payload)) raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) - _last_slash_idx = data_raw.headers['Location'].rindex('/') - return data_raw.headers['Location'][_last_slash_idx + 1:] + _last_slash_idx = data_raw.headers["Location"].rindex("/") + return data_raw.headers["Location"][_last_slash_idx + 1 :] def users_count(self): """ @@ -490,8 +565,7 @@ class KeycloakAdmin: :return: Http response """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_put(URL_ADMIN_USER.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put(URL_ADMIN_USER.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_user(self, user_id): @@ -522,8 +596,9 @@ class KeycloakAdmin: """ payload = {"type": "password", "temporary": temporary, "value": password} params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_put(URL_ADMIN_RESET_PASSWORD.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put( + URL_ADMIN_RESET_PASSWORD.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_credentials(self, user_id): @@ -551,7 +626,11 @@ class KeycloakAdmin: :param: credential_id: credential id :return: Keycloak server response (ClientRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": user_id, "credential_id": credential_id} + params_path = { + "realm-name": self.realm_name, + "id": user_id, + "credential_id": credential_id, + } data_raw = self.raw_get(URL_ADMIN_USER_CREDENTIAL.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) @@ -566,7 +645,11 @@ class KeycloakAdmin: :param: credential_id: credential id :return: Keycloak server response (ClientRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": user_id, "credential_id": credential_id} + params_path = { + "realm-name": self.realm_name, + "id": user_id, + "credential_id": credential_id, + } data_raw = self.raw_delete(URL_ADMIN_USER_CREDENTIAL.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) @@ -615,9 +698,15 @@ class KeycloakAdmin: :param provider_username: username specified by the provider :return: """ - payload = {"identityProvider": provider_id, "userId": provider_userid, "userName": provider_username} + payload = { + "identityProvider": provider_id, + "userId": provider_userid, + "userName": provider_username, + } params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} - data_raw = self.raw_post(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), data=json.dumps(payload) + ) def delete_user_social_login(self, user_id, provider_id): @@ -631,7 +720,9 @@ class KeycloakAdmin: data_raw = self.raw_delete(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def send_update_account(self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None): + 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. @@ -646,8 +737,11 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": user_id} params_query = {"client_id": client_id, "lifespan": lifespan, "redirect_uri": redirect_uri} - data_raw = self.raw_put(URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), - data=json.dumps(payload), **params_query) + data_raw = self.raw_put( + URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), + data=json.dumps(payload), + **params_query + ) return raise_error_from_response(data_raw, KeycloakGetError) def send_verify_email(self, user_id, client_id=None, redirect_uri=None): @@ -663,8 +757,9 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": user_id} params_query = {"client_id": client_id, "redirect_uri": redirect_uri} - data_raw = self.raw_put(URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), - data={}, **params_query) + data_raw = self.raw_put( + URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), data={}, **params_query + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_sessions(self, user_id): @@ -740,7 +835,7 @@ class KeycloakAdmin: """ for subgroup in group["subGroups"]: - if subgroup['path'] == path: + if subgroup["path"] == path: return subgroup elif subgroup["subGroups"]: for subgroup in group["subGroups"]: @@ -787,11 +882,11 @@ class KeycloakAdmin: # TODO: Review this code is necessary for group in groups: - if group['path'] == path: + if group["path"] == path: return group elif search_in_subgroups and group["subGroups"]: for group in group["subGroups"]: - if group['path'] == path: + if group["path"] == path: return group res = self.get_subgroups(group, path) if res != None: @@ -814,14 +909,18 @@ class KeycloakAdmin: if parent is None: params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_GROUPS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_GROUPS.format(**params_path), data=json.dumps(payload) + ) else: - params_path = {"realm-name": self.realm_name, "id": parent, } - data_raw = self.raw_post(URL_ADMIN_GROUP_CHILD.format(**params_path), - data=json.dumps(payload)) + params_path = {"realm-name": self.realm_name, "id": parent} + data_raw = self.raw_post( + URL_ADMIN_GROUP_CHILD.format(**params_path), data=json.dumps(payload) + ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def update_group(self, group_id, payload): """ @@ -837,8 +936,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_put(URL_ADMIN_GROUP.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put(URL_ADMIN_GROUP.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def group_set_permissions(self, group_id, enabled=True): @@ -851,8 +949,10 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_put(URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), - data=json.dumps({"enabled": enabled})) + data_raw = self.raw_put( + URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), + data=json.dumps({"enabled": enabled}), + ) return raise_error_from_response(data_raw, KeycloakGetError) def group_user_add(self, user_id, group_id): @@ -935,7 +1035,7 @@ class KeycloakAdmin: clients = self.get_clients() for client in clients: - if client_name == client.get('name') or client_name == client.get('clientId'): + if client_name == client.get("name") or client_name == client.get("clientId"): return client["id"] return None @@ -965,12 +1065,14 @@ class KeycloakAdmin: :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, - "id": client_id} + params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post( + URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def get_client_authz_resources(self, client_id): """ @@ -1008,12 +1110,15 @@ class KeycloakAdmin: :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, - "id": client_id} + params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post( + URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def create_client_authz_resource_based_permission(self, client_id, payload, skip_exists=False): """ @@ -1039,12 +1144,15 @@ class KeycloakAdmin: :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, - "id": client_id} + params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post( + URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def get_client_authz_scopes(self, client_id): """ @@ -1110,9 +1218,10 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(payload)) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def update_client(self, client_id, payload): """ @@ -1124,8 +1233,7 @@ class KeycloakAdmin: :return: Http response """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_put(URL_ADMIN_CLIENT.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put(URL_ADMIN_CLIENT.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_client(self, client_id): @@ -1182,7 +1290,7 @@ class KeycloakAdmin: :param query: Additional Query parameters (see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_roles_resource) :return: Keycloak Server Response (UserRepresentation) """ - params_path = {"realm-name": self.realm_name, "role-name":role_name} + params_path = {"realm-name": self.realm_name, "role-name": role_name} return self.__fetch_all(URL_ADMIN_REALM_ROLES_MEMBERS.format(**params_path), query) def get_client_roles(self, client_id): @@ -1250,9 +1358,12 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": client_role_id} - data_raw = self.raw_post(URL_ADMIN_CLIENT_ROLES.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post( + URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): """ @@ -1266,8 +1377,10 @@ class KeycloakAdmin: 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(URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE.format(**params_path), + data=json.dumps(payload), + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_client_role(self, client_role_id, role_name): @@ -1296,8 +1409,9 @@ class KeycloakAdmin: 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(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_client_role_members(self, client_id, role_name, **query): @@ -1308,9 +1422,8 @@ class KeycloakAdmin: :param query: Additional query parameters ( see https://www.keycloak.org/docs-api/11.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} - return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_MEMBERS.format(**params_path) , query) - + params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} + return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_MEMBERS.format(**params_path), query) def create_realm_role(self, payload, skip_exists=False): """ @@ -1322,9 +1435,12 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_REALM_ROLES.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post( + URL_ADMIN_REALM_ROLES.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def get_realm_role(self, role_name): """ @@ -1348,8 +1464,9 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "role-name": role_name} - data_raw = self.raw_put(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put( + URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_realm_role(self, role_name): @@ -1360,8 +1477,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "role-name": role_name} - data_raw = self.raw_delete( - URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) + data_raw = self.raw_delete(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def add_composite_realm_roles_to_role(self, role_name, roles): @@ -1377,9 +1493,9 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_post( URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, - expected_codes=[204]) + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def remove_composite_realm_roles_to_role(self, role_name, roles): """ @@ -1394,9 +1510,9 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_delete( URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, - expected_codes=[204]) + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_composite_realm_roles_of_role(self, role_name): """ @@ -1407,8 +1523,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "role-name": role_name} - data_raw = self.raw_get( - URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def assign_realm_roles(self, user_id, roles): @@ -1422,8 +1537,9 @@ class KeycloakAdmin: payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_post(URL_ADMIN_USER_REALM_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_realm_roles_of_user(self, user_id, roles): @@ -1437,8 +1553,9 @@ class KeycloakAdmin: payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_delete(URL_ADMIN_USER_REALM_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_delete( + URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_realm_roles_of_user(self, user_id): @@ -1484,8 +1601,9 @@ class KeycloakAdmin: payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_post(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_group_realm_roles(self, group_id, roles): @@ -1499,8 +1617,9 @@ class KeycloakAdmin: payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_delete(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_delete( + URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_group_realm_roles(self, group_id): @@ -1526,8 +1645,9 @@ class KeycloakAdmin: 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(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_group_client_roles(self, group_id, client_id): @@ -1555,8 +1675,9 @@ class KeycloakAdmin: 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(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_delete( + URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_client_roles_of_user(self, user_id, client_id): @@ -1577,7 +1698,9 @@ class KeycloakAdmin: :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ - return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id) + return self._get_client_roles_of_user( + URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id + ) def get_composite_client_roles_of_user(self, user_id, client_id): """ @@ -1587,7 +1710,9 @@ class KeycloakAdmin: :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ - return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id) + return self._get_client_roles_of_user( + URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id + ) def _get_client_roles_of_user(self, client_level_role_mapping_url, user_id, client_id): params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} @@ -1605,8 +1730,9 @@ class KeycloakAdmin: """ 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_delete(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_delete( + URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_authentication_flows(self): @@ -1649,9 +1775,10 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path), data=json.dumps(payload)) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def copy_authentication_flow(self, payload, flow_alias): """ @@ -1663,8 +1790,9 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.raw_post(URL_ADMIN_FLOWS_COPY.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_FLOWS_COPY.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def delete_authentication_flow(self, flow_id): @@ -1705,9 +1833,10 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[202,204]) + data_raw = self.raw_put( + URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[202, 204]) def get_authentication_flow_execution(self, execution_id): """ @@ -1736,8 +1865,9 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def delete_authentication_flow_execution(self, execution_id): @@ -1768,9 +1898,12 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post( + URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def get_authenticator_config(self, config_id): """ @@ -1795,8 +1928,9 @@ class KeycloakAdmin: :return: Response(json) """ params_path = {"realm-name": self.realm_name, "id": config_id} - data_raw = self.raw_put(URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put( + URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_authenticator_config(self, config_id): @@ -1821,12 +1955,13 @@ class KeycloakAdmin: :param action: Action can be "triggerFullSync" or "triggerChangedUsersSync" :return: """ - data = {'action': action} + data = {"action": action} params_query = {"action": action} params_path = {"realm-name": self.realm_name, "id": storage_id} - data_raw = self.raw_post(URL_ADMIN_USER_STORAGE.format(**params_path), - data=json.dumps(data), **params_query) + data_raw = self.raw_post( + URL_ADMIN_USER_STORAGE.format(**params_path), data=json.dumps(data), **params_query + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_scopes(self): @@ -1866,9 +2001,12 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_CLIENT_SCOPES.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post( + URL_ADMIN_CLIENT_SCOPES.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def update_client_scope(self, client_scope_id, payload): """ @@ -1882,8 +2020,9 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} - data_raw = self.raw_put(URL_ADMIN_CLIENT_SCOPE.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put( + URL_ADMIN_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def add_mapper_to_client_scope(self, client_scope_id, payload): @@ -1899,7 +2038,8 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), data=json.dumps(payload)) + URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) @@ -1913,11 +2053,13 @@ class KeycloakAdmin: :return: Keycloak server Response """ - params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id, - "protocol-mapper-id": protocol_mppaer_id} + params_path = { + "realm-name": self.realm_name, + "scope-id": client_scope_id, + "protocol-mapper-id": protocol_mppaer_id, + } - data_raw = self.raw_delete( - URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path)) + data_raw = self.raw_delete(URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) @@ -1933,11 +2075,15 @@ class KeycloakAdmin: :return: Keycloak server Response """ - params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id, - "protocol-mapper-id": protocol_mapper_id} + params_path = { + "realm-name": self.realm_name, + "scope-id": client_scope_id, + "protocol-mapper-id": protocol_mapper_id, + } data_raw = self.raw_put( - URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path), data=json.dumps(payload)) + URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) @@ -1951,7 +2097,6 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def delete_default_default_client_scope(self, scope_id): """ Delete default default client scope @@ -1963,7 +2108,6 @@ class KeycloakAdmin: data_raw = self.raw_delete(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def add_default_default_client_scope(self, scope_id): """ Add default default client scope @@ -1973,10 +2117,11 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": scope_id} payload = {"realm": self.realm_name, "clientScopeId": scope_id} - data_raw = self.raw_put(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_put( + URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def get_default_optional_client_scopes(self): """ Return list of default optional client scopes @@ -1987,7 +2132,6 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def delete_default_optional_client_scope(self, scope_id): """ Delete default optional client scope @@ -1999,7 +2143,6 @@ class KeycloakAdmin: data_raw = self.raw_delete(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def add_default_optional_client_scope(self, scope_id): """ Add default optional client scope @@ -2009,10 +2152,11 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": scope_id} payload = {"realm": self.realm_name, "clientScopeId": scope_id} - data_raw = self.raw_put(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_put( + URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def add_mapper_to_client(self, client_id, payload): """ Add a mapper to a client @@ -2026,28 +2170,30 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_PROTOCOL_MAPPERS.format(**params_path), data=json.dumps(payload)) + URL_ADMIN_CLIENT_PROTOCOL_MAPPERS.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) - + def update_client_mapper(self, client_id, mapper_id, payload): """ 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 + :return: Keycloak server response """ params_path = { "realm-name": self.realm_name, - "id": self.client_id, + "id": self.client_id, "protocol-mapper-id": mapper_id, } data_raw = self.raw_put( - URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), data=json.dumps(payload)) - + URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def remove_client_mapper(self, client_id, client_mapper_id): @@ -2062,14 +2208,13 @@ class KeycloakAdmin: params_path = { "realm-name": self.realm_name, "id": client_id, - "protocol-mapper-id": client_mapper_id + "protocol-mapper-id": client_mapper_id, } - data_raw = self.raw_delete( - URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path)) - + data_raw = self.raw_delete(URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - + def generate_client_secrets(self, client_id): """ @@ -2109,8 +2254,7 @@ class KeycloakAdmin: :return: components list """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_COMPONENTS.format(**params_path), - data=None, **query) + data_raw = self.raw_get(URL_ADMIN_COMPONENTS.format(**params_path), data=None, **query) return raise_error_from_response(data_raw, KeycloakGetError) def create_component(self, payload): @@ -2126,8 +2270,9 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_COMPONENTS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_COMPONENTS.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def get_component(self, component_id): @@ -2156,8 +2301,9 @@ class KeycloakAdmin: :return: Http response """ params_path = {"realm-name": self.realm_name, "component-id": component_id} - data_raw = self.raw_put(URL_ADMIN_COMPONENT.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put( + URL_ADMIN_COMPONENT.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_component(self, component_id): @@ -2182,8 +2328,7 @@ class KeycloakAdmin: :return: keys list """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_KEYS.format(**params_path), - data=None) + data_raw = self.raw_get(URL_ADMIN_KEYS.format(**params_path), data=None) return raise_error_from_response(data_raw, KeycloakGetError) def get_events(self, query=None): @@ -2196,8 +2341,7 @@ class KeycloakAdmin: :return: events list """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_EVENTS.format(**params_path), - data=None, **query) + data_raw = self.raw_get(URL_ADMIN_EVENTS.format(**params_path), data=None, **query) return raise_error_from_response(data_raw, KeycloakGetError) def set_events(self, payload): @@ -2210,8 +2354,7 @@ class KeycloakAdmin: :return: Http response """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_put(URL_ADMIN_EVENTS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put(URL_ADMIN_EVENTS.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def raw_get(self, *args, **kwargs): @@ -2222,7 +2365,7 @@ class KeycloakAdmin: and try *get* once more. """ r = self.connection.raw_get(*args, **kwargs) - if 'get' in self.auto_refresh_token and r.status_code == 401: + if "get" in self.auto_refresh_token and r.status_code == 401: self.refresh_token() return self.connection.raw_get(*args, **kwargs) return r @@ -2235,7 +2378,7 @@ class KeycloakAdmin: and try *post* once more. """ r = self.connection.raw_post(*args, **kwargs) - if 'post' in self.auto_refresh_token and r.status_code == 401: + if "post" in self.auto_refresh_token and r.status_code == 401: self.refresh_token() return self.connection.raw_post(*args, **kwargs) return r @@ -2248,7 +2391,7 @@ class KeycloakAdmin: and try *put* once more. """ r = self.connection.raw_put(*args, **kwargs) - if 'put' in self.auto_refresh_token and r.status_code == 401: + if "put" in self.auto_refresh_token and r.status_code == 401: self.refresh_token() return self.connection.raw_put(*args, **kwargs) return r @@ -2261,7 +2404,7 @@ class KeycloakAdmin: and try *delete* once more. """ r = self.connection.raw_delete(*args, **kwargs) - if 'delete' in self.auto_refresh_token and r.status_code == 401: + if "delete" in self.auto_refresh_token and r.status_code == 401: self.refresh_token() return self.connection.raw_delete(*args, **kwargs) return r @@ -2273,11 +2416,15 @@ class KeycloakAdmin: token_realm_name = self.realm_name else: token_realm_name = "master" - - self.keycloak_openid = KeycloakOpenID(server_url=self.server_url, client_id=self.client_id, - realm_name=token_realm_name, verify=self.verify, - client_secret_key=self.client_secret_key, - custom_headers=self.custom_headers) + + self.keycloak_openid = KeycloakOpenID( + server_url=self.server_url, + client_id=self.client_id, + realm_name=token_realm_name, + verify=self.verify, + client_secret_key=self.client_secret_key, + custom_headers=self.custom_headers, + ) grant_type = ["password"] if self.client_secret_key: @@ -2286,11 +2433,13 @@ class KeycloakAdmin: self.realm_name = self.user_realm_name if self.username and self.password: - self._token = self.keycloak_openid.token(self.username, self.password, grant_type=grant_type) + self._token = self.keycloak_openid.token( + self.username, self.password, grant_type=grant_type + ) headers = { - 'Authorization': 'Bearer ' + self.token.get('access_token'), - 'Content-Type': 'application/json' + "Authorization": "Bearer " + self.token.get("access_token"), + "Content-Type": "application/json", } else: self._token = None @@ -2300,13 +2449,12 @@ class KeycloakAdmin: # merge custom headers to main headers headers.update(self.custom_headers) - self._connection = ConnectionManager(base_url=self.server_url, - headers=headers, - timeout=60, - verify=self.verify) + self._connection = ConnectionManager( + base_url=self.server_url, headers=headers, timeout=60, verify=self.verify + ) def refresh_token(self): - refresh_token = self.token.get('refresh_token', None) + refresh_token = self.token.get("refresh_token", None) if refresh_token is None: self.get_token() else: @@ -2314,16 +2462,18 @@ class KeycloakAdmin: self.token = self.keycloak_openid.refresh_token(refresh_token) except KeycloakGetError as e: list_errors = [ - b'Refresh token expired', - b'Token is not active', - b'Session not active' + b"Refresh token expired", + b"Token is not active", + b"Session not active", ] if e.response_code == 400 and any(err in e.response_body for err in list_errors): - self.get_token() + self.get_token() else: raise - - self.connection.add_param_headers('Authorization', 'Bearer ' + self.token.get('access_token')) + + self.connection.add_param_headers( + "Authorization", "Bearer " + self.token.get("access_token") + ) def get_client_all_sessions(self, client_id): """ @@ -2346,9 +2496,10 @@ class KeycloakAdmin: DELETE admin/realms/{realm-name}/users/{id}/role-mappings/realm """ - params_path = {"realm-name": self.realm_name, "id": str(user_id) } - data_raw = self.raw_delete(URL_ADMIN_DELETE_USER_ROLE.format(**params_path), - data=json.dumps(payload)) + params_path = {"realm-name": self.realm_name, "id": str(user_id)} + data_raw = self.raw_delete( + URL_ADMIN_DELETE_USER_ROLE.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_client_sessions_stats(self): @@ -2360,8 +2511,5 @@ class KeycloakAdmin: :return: Dict of clients and session count """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get( - self.URL_ADMIN_CLIENT_SESSION_STATS.format(**params_path) - ) + data_raw = self.raw_get(self.URL_ADMIN_CLIENT_SESSION_STATS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index 1d6ed28..d9c068f 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -27,24 +27,38 @@ from jose import jwt from .authorization import Authorization from .connection import ConnectionManager -from .exceptions import raise_error_from_response, KeycloakGetError, \ - KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError, KeycloakDeprecationError +from .exceptions import ( + KeycloakAuthorizationConfigError, + KeycloakDeprecationError, + KeycloakGetError, + KeycloakInvalidTokenError, + KeycloakRPTNotFound, + raise_error_from_response, +) from .urls_patterns import ( - URL_REALM, URL_AUTH, + URL_CERTS, + URL_ENTITLEMENT, + URL_INTROSPECT, + URL_LOGOUT, + URL_REALM, URL_TOKEN, URL_USERINFO, URL_WELL_KNOWN, - URL_LOGOUT, - URL_CERTS, - URL_ENTITLEMENT, - URL_INTROSPECT ) class KeycloakOpenID: - - def __init__(self, server_url, realm_name, client_id, client_secret_key=None, verify=True, custom_headers=None, proxies=None): + def __init__( + self, + server_url, + realm_name, + client_id, + client_secret_key=None, + verify=True, + custom_headers=None, + proxies=None, + ): """ :param server_url: Keycloak server url @@ -62,11 +76,9 @@ class KeycloakOpenID: if custom_headers is not None: # merge custom headers to main headers headers.update(custom_headers) - self._connection = ConnectionManager(base_url=server_url, - headers=headers, - timeout=60, - verify=verify, - proxies=proxies) + self._connection = ConnectionManager( + base_url=server_url, headers=headers, timeout=60, verify=verify, proxies=proxies + ) self._authorization = Authorization() @@ -138,7 +150,7 @@ class KeycloakOpenID: :param kwargs: :return: """ - if method_token_info == 'introspect': + if method_token_info == "introspect": token_info = self.introspect(token) else: token_info = self.decode_token(token, **kwargs) @@ -146,11 +158,11 @@ class KeycloakOpenID: return token_info def well_know(self): - """ 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. + """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. + :return It lists endpoints and other configuration options relevant. """ params_path = {"realm-name": self.realm_name} @@ -165,12 +177,23 @@ class KeycloakOpenID: :return: """ - params_path = {"authorization-endpoint": self.well_know()['authorization_endpoint'], - "client-id": self.client_id, - "redirect-uri": redirect_uri} + params_path = { + "authorization-endpoint": self.well_know()["authorization_endpoint"], + "client-id": self.client_id, + "redirect-uri": redirect_uri, + } return URL_AUTH.format(**params_path) - def token(self, username="", password="", grant_type=["password"], code="", redirect_uri="", totp=None, **extra): + def token( + self, + username="", + password="", + grant_type=["password"], + code="", + redirect_uri="", + totp=None, + **extra + ): """ 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 @@ -188,9 +211,14 @@ class KeycloakOpenID: :return: """ params_path = {"realm-name": self.realm_name} - payload = {"username": username, "password": password, - "client_id": self.client_id, "grant_type": grant_type, - "code": code, "redirect_uri": redirect_uri} + payload = { + "username": username, + "password": password, + "client_id": self.client_id, + "grant_type": grant_type, + "code": code, + "redirect_uri": redirect_uri, + } if extra: payload.update(extra) @@ -198,8 +226,7 @@ class KeycloakOpenID: payload["totp"] = totp payload = self._add_secret_key(payload) - data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), - data=payload) + data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakGetError) def refresh_token(self, refresh_token, grant_type=["refresh_token"]): @@ -216,10 +243,13 @@ class KeycloakOpenID: :return: """ params_path = {"realm-name": self.realm_name} - payload = {"client_id": self.client_id, "grant_type": grant_type, "refresh_token": refresh_token} + payload = { + "client_id": self.client_id, + "grant_type": grant_type, + "refresh_token": refresh_token, + } payload = self._add_secret_key(payload) - data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), - data=payload) + data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakGetError) def userinfo(self, token): @@ -250,8 +280,7 @@ class KeycloakOpenID: payload = {"client_id": self.client_id, "refresh_token": refresh_token} payload = self._add_secret_key(payload) - data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), - data=payload) + data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) @@ -268,7 +297,7 @@ class KeycloakOpenID: params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - + def public_key(self): """ The public key is exposed by the realm page directly. @@ -277,8 +306,7 @@ class KeycloakOpenID: """ params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_get(URL_REALM.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError)['public_key'] - + return raise_error_from_response(data_raw, KeycloakGetError)["public_key"] def entitlement(self, token, resource_server_id): """ @@ -293,8 +321,8 @@ class KeycloakOpenID: self.connection.add_param_headers("Authorization", "Bearer " + token) params_path = {"realm-name": self.realm_name, "resource-server-id": resource_server_id} data_raw = self.connection.raw_get(URL_ENTITLEMENT.format(**params_path)) - - if data_raw.status_code == 404: + + if data_raw.status_code == 404: return raise_error_from_response(data_raw, KeycloakDeprecationError) return raise_error_from_response(data_raw, KeycloakGetError) @@ -316,7 +344,7 @@ class KeycloakOpenID: payload = {"client_id": self.client_id, "token": token} - if token_type_hint == 'requesting_party_token': + if token_type_hint == "requesting_party_token": if rpt: payload.update({"token": rpt, "token_type_hint": token_type_hint}) self.connection.add_param_headers("Authorization", "Bearer " + token) @@ -325,12 +353,11 @@ class KeycloakOpenID: payload = self._add_secret_key(payload) - data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), - data=payload) + data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakGetError) - def decode_token(self, token, key, algorithms=['RS256'], **kwargs): + def decode_token(self, token, key, algorithms=["RS256"], **kwargs): """ A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key. This specification @@ -347,8 +374,7 @@ class KeycloakOpenID: :return: """ - return jwt.decode(token, key, algorithms=algorithms, - audience=self.client_id, **kwargs) + return jwt.decode(token, key, algorithms=algorithms, audience=self.client_id, **kwargs) def load_authorization_config(self, path): """ @@ -357,12 +383,12 @@ class KeycloakOpenID: :param path: settings file (json) :return: """ - authorization_file = open(path, 'r') + authorization_file = open(path, "r") authorization_json = json.loads(authorization_file.read()) self.authorization.load_config(authorization_json) authorization_file.close() - def get_policies(self, token, method_token_info='introspect', **kwargs): + def get_policies(self, token, method_token_info="introspect", **kwargs): """ Get policies by user token @@ -377,12 +403,10 @@ class KeycloakOpenID: token_info = self._token_info(token, method_token_info, **kwargs) - if method_token_info == 'introspect' and not token_info['active']: - raise KeycloakInvalidTokenError( - "Token expired or invalid." - ) + if method_token_info == "introspect" and not token_info["active"]: + raise KeycloakInvalidTokenError("Token expired or invalid.") - user_resources = token_info['resource_access'].get(self.client_id) + user_resources = token_info["resource_access"].get(self.client_id) if not user_resources: return None @@ -390,13 +414,13 @@ class KeycloakOpenID: policies = [] for policy_name, policy in self.authorization.policies.items(): - for role in user_resources['roles']: + for role in user_resources["roles"]: if self._build_name_role(role) in policy.roles: policies.append(policy) return list(set(policies)) - def get_permissions(self, token, method_token_info='introspect', **kwargs): + def get_permissions(self, token, method_token_info="introspect", **kwargs): """ Get permission by user token @@ -413,12 +437,10 @@ class KeycloakOpenID: token_info = self._token_info(token, method_token_info, **kwargs) - if method_token_info == 'introspect' and not token_info['active']: - raise KeycloakInvalidTokenError( - "Token expired or invalid." - ) + if method_token_info == "introspect" and not token_info["active"]: + raise KeycloakInvalidTokenError("Token expired or invalid.") - user_resources = token_info['resource_access'].get(self.client_id) + user_resources = token_info["resource_access"].get(self.client_id) if not user_resources: return None @@ -426,7 +448,7 @@ class KeycloakOpenID: permissions = [] for policy_name, policy in self.authorization.policies.items(): - for role in user_resources['roles']: + for role in user_resources["roles"]: if self._build_name_role(role) in policy.roles: permissions += policy.permissions diff --git a/keycloak/tests/test_connection.py b/keycloak/tests/test_connection.py deleted file mode 100644 index cb98feb..0000000 --- a/keycloak/tests/test_connection.py +++ /dev/null @@ -1,191 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2017 Marcos Pereira -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -from unittest import mock - -from httmock import urlmatch, response, HTTMock, all_requests - -from keycloak import KeycloakAdmin, KeycloakOpenID -from ..connection import ConnectionManager - -try: - import unittest -except ImportError: - import unittest2 as unittest - - -class TestConnection(unittest.TestCase): - - def setUp(self): - self._conn = ConnectionManager( - base_url="http://localhost/", - headers={}, - timeout=60) - - @all_requests - def response_content_success(self, url, request): - headers = {'content-type': 'application/json'} - content = b'response_ok' - return response(200, content, headers, None, 5, request) - - def test_raw_get(self): - with HTTMock(self.response_content_success): - resp = self._conn.raw_get("/known_path") - self.assertEqual(resp.content, b'response_ok') - self.assertEqual(resp.status_code, 200) - - def test_raw_post(self): - @urlmatch(path="/known_path", method="post") - def response_post_success(url, request): - headers = {'content-type': 'application/json'} - content = 'response'.encode("utf-8") - return response(201, content, headers, None, 5, request) - - with HTTMock(response_post_success): - resp = self._conn.raw_post("/known_path", - {'field': 'value'}) - self.assertEqual(resp.content, b'response') - self.assertEqual(resp.status_code, 201) - - def test_raw_put(self): - @urlmatch(netloc="localhost", path="/known_path", method="put") - def response_put_success(url, request): - headers = {'content-type': 'application/json'} - content = 'response'.encode("utf-8") - return response(200, content, headers, None, 5, request) - - with HTTMock(response_put_success): - resp = self._conn.raw_put("/known_path", - {'field': 'value'}) - self.assertEqual(resp.content, b'response') - self.assertEqual(resp.status_code, 200) - - def test_raw_get_fail(self): - @urlmatch(netloc="localhost", path="/known_path", method="get") - def response_get_fail(url, request): - headers = {'content-type': 'application/json'} - content = "404 page not found".encode("utf-8") - return response(404, content, headers, None, 5, request) - - with HTTMock(response_get_fail): - resp = self._conn.raw_get("/known_path") - - self.assertEqual(resp.content, b"404 page not found") - self.assertEqual(resp.status_code, 404) - - def test_raw_post_fail(self): - @urlmatch(netloc="localhost", path="/known_path", method="post") - def response_post_fail(url, request): - headers = {'content-type': 'application/json'} - content = str(["Start can't be blank"]).encode("utf-8") - return response(404, content, headers, None, 5, request) - - with HTTMock(response_post_fail): - resp = self._conn.raw_post("/known_path", - {'field': 'value'}) - self.assertEqual(resp.content, str(["Start can't be blank"]).encode("utf-8")) - self.assertEqual(resp.status_code, 404) - - def test_raw_put_fail(self): - @urlmatch(netloc="localhost", path="/known_path", method="put") - def response_put_fail(url, request): - headers = {'content-type': 'application/json'} - content = str(["Start can't be blank"]).encode("utf-8") - return response(404, content, headers, None, 5, request) - - with HTTMock(response_put_fail): - resp = self._conn.raw_put("/known_path", - {'field': 'value'}) - self.assertEqual(resp.content, str(["Start can't be blank"]).encode("utf-8")) - self.assertEqual(resp.status_code, 404) - - def test_add_param_headers(self): - self._conn.add_param_headers("test", "value") - self.assertEqual(self._conn.headers, - {"test": "value"}) - - def test_del_param_headers(self): - self._conn.add_param_headers("test", "value") - self._conn.del_param_headers("test") - self.assertEqual(self._conn.headers, {}) - - def test_clean_param_headers(self): - self._conn.add_param_headers("test", "value") - self.assertEqual(self._conn.headers, - {"test": "value"}) - self._conn.clean_headers() - self.assertEqual(self._conn.headers, {}) - - def test_exist_param_headers(self): - self._conn.add_param_headers("test", "value") - self.assertTrue(self._conn.exist_param_headers("test")) - self.assertFalse(self._conn.exist_param_headers("test_no")) - - def test_get_param_headers(self): - self._conn.add_param_headers("test", "value") - self.assertTrue(self._conn.exist_param_headers("test")) - self.assertFalse(self._conn.exist_param_headers("test_no")) - - def test_get_headers(self): - self._conn.add_param_headers("test", "value") - self.assertEqual(self._conn.headers, - {"test": "value"}) - - def test_KeycloakAdmin_custom_header(self): - - class FakeToken: - @staticmethod - def get(string_val): - return "faketoken" - - fake_token = FakeToken() - - with mock.patch.object(KeycloakOpenID, "__init__", return_value=None) as mock_keycloak_open_id: - with mock.patch("keycloak.keycloak_openid.KeycloakOpenID.token", return_value=fake_token): - with mock.patch("keycloak.connection.ConnectionManager.__init__", return_value=None) as mock_connection_manager: - with mock.patch("keycloak.connection.ConnectionManager.__del__", return_value=None) as mock_connection_manager_delete: - server_url = "https://localhost/auth/" - username = "admin" - password = "secret" - realm_name = "master" - - headers = { - 'Custom': 'test-custom-header' - } - KeycloakAdmin(server_url=server_url, - username=username, - password=password, - realm_name=realm_name, - verify=False, - custom_headers=headers) - - mock_keycloak_open_id.assert_called_with(server_url=server_url, - realm_name=realm_name, - client_id='admin-cli', - client_secret_key=None, - verify=False, - custom_headers=headers) - - expected_header = {'Authorization': 'Bearer faketoken', - 'Content-Type': 'application/json', - 'Custom': 'test-custom-header' - } - - mock_connection_manager.assert_called_with(base_url=server_url, - headers=expected_header, - timeout=60, - verify=False) - mock_connection_manager_delete.assert_called_once_with() diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index d7dd16a..43699eb 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -30,7 +30,9 @@ URL_LOGOUT = "realms/{realm-name}/protocol/openid-connect/logout" URL_CERTS = "realms/{realm-name}/protocol/openid-connect/certs" URL_INTROSPECT = "realms/{realm-name}/protocol/openid-connect/token/introspect" URL_ENTITLEMENT = "realms/{realm-name}/authz/entitlement/{resource-server-id}" -URL_AUTH = "{authorization-endpoint}?client_id={client-id}&response_type=code&redirect_uri={redirect-uri}" +URL_AUTH = ( + "{authorization-endpoint}?client_id={client-id}&response_type=code&redirect_uri={redirect-uri}" +) # ADMIN URLS URL_ADMIN_USERS = "admin/realms/{realm-name}/users" @@ -41,14 +43,26 @@ URL_ADMIN_SEND_UPDATE_ACCOUNT = "admin/realms/{realm-name}/users/{id}/execute-ac URL_ADMIN_SEND_VERIFY_EMAIL = "admin/realms/{realm-name}/users/{id}/send-verify-email" URL_ADMIN_RESET_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password" URL_ADMIN_GET_SESSIONS = "admin/realms/{realm-name}/users/{id}/sessions" -URL_ADMIN_USER_CLIENT_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}" +URL_ADMIN_USER_CLIENT_ROLES = ( + "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}" +) URL_ADMIN_USER_REALM_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" -URL_ADMIN_USER_REALM_ROLES_AVAILABLE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm/available" -URL_ADMIN_USER_REALM_ROLES_COMPOSITE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm/composite" +URL_ADMIN_USER_REALM_ROLES_AVAILABLE = ( + "admin/realms/{realm-name}/users/{id}/role-mappings/realm/available" +) +URL_ADMIN_USER_REALM_ROLES_COMPOSITE = ( + "admin/realms/{realm-name}/users/{id}/role-mappings/realm/composite" +) URL_ADMIN_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/realm" -URL_ADMIN_GROUPS_CLIENT_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/clients/{client-id}" -URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/available" -URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/composite" +URL_ADMIN_GROUPS_CLIENT_ROLES = ( + "admin/realms/{realm-name}/groups/{id}/role-mappings/clients/{client-id}" +) +URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE = ( + "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/available" +) +URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE = ( + "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/composite" +) URL_ADMIN_USER_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}" URL_ADMIN_USER_GROUPS = "admin/realms/{realm-name}/users/{id}/groups" URL_ADMIN_USER_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password" @@ -79,8 +93,12 @@ URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/re URL_ADMIN_CLIENT_AUTHZ_SCOPES = URL_ADMIN_CLIENT + "/authz/resource-server/scope?max=-1" URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS = URL_ADMIN_CLIENT + "/authz/resource-server/permission?max=-1" URL_ADMIN_CLIENT_AUTHZ_POLICIES = URL_ADMIN_CLIENT + "/authz/resource-server/policy?max=-1" -URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY = URL_ADMIN_CLIENT + "/authz/resource-server/policy/role?max=-1" -URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION = URL_ADMIN_CLIENT + "/authz/resource-server/permission/resource?max=-1" +URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY = ( + URL_ADMIN_CLIENT + "/authz/resource-server/policy/role?max=-1" +) +URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION = ( + URL_ADMIN_CLIENT + "/authz/resource-server/permission/resource?max=-1" +) URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" @@ -101,7 +119,9 @@ URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" URL_ADMIN_IDP_MAPPERS = "admin/realms/{realm-name}/identity-provider/instances/{idp-alias}/mappers" URL_ADMIN_IDP = "admin/realms//{realm-name}/identity-provider/instances/{alias}" URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" -URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = "admin/realms/{realm-name}/roles/{role-name}/composites" +URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = ( + "admin/realms/{realm-name}/roles/{role-name}/composites" +) URL_ADMIN_REALM_EXPORT = "admin/realms/{realm-name}/partial-export?exportClients={export-clients}&exportGroupsAndRoles={export-groups-and-roles}" URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES = URL_ADMIN_REALM + "/default-default-client-scopes" @@ -113,10 +133,16 @@ URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOW = URL_ADMIN_FLOWS + "/{id}" URL_ADMIN_FLOWS_ALIAS = "admin/realms/{realm-name}/authentication/flows/{flow-id}" URL_ADMIN_FLOWS_COPY = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/copy" -URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" +URL_ADMIN_FLOWS_EXECUTIONS = ( + "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" +) URL_ADMIN_FLOWS_EXECUTION = "admin/realms/{realm-name}/authentication/executions/{id}" -URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" -URL_ADMIN_FLOWS_EXECUTIONS_FLOW = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" +URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION = ( + "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" +) +URL_ADMIN_FLOWS_EXECUTIONS_FLOW = ( + "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" +) URL_ADMIN_AUTHENTICATOR_CONFIG = "admin/realms/{realm-name}/authentication/config/{id}" URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" @@ -124,10 +150,11 @@ URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}" URL_ADMIN_KEYS = "admin/realms/{realm-name}/keys" URL_ADMIN_USER_FEDERATED_IDENTITIES = "admin/realms/{realm-name}/users/{id}/federated-identity" -URL_ADMIN_USER_FEDERATED_IDENTITY = "admin/realms/{realm-name}/users/{id}/federated-identity/{provider}" +URL_ADMIN_USER_FEDERATED_IDENTITY = ( + "admin/realms/{realm-name}/users/{id}/federated-identity/{provider}" +) -URL_ADMIN_EVENTS = 'admin/realms/{realm-name}/events' +URL_ADMIN_EVENTS = "admin/realms/{realm-name}/events" URL_ADMIN_DELETE_USER_ROLE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" URL_ADMIN_CLIENT_SESSION_STATS = "admin/realms/{realm-name}/client-session-stats" - diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f947450 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[tool.black] +line-length = 99 + +[tool.isort] +line_length = 99 +profile = "black" diff --git a/requirements.txt b/requirements.txt index a353c7f..5474982 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,3 @@ requests>=2.20.0 -httmock>=1.2.5 python-jose>=1.4.0 -twine==1.13.0 -jose~=1.0.0 -setuptools~=54.2.0 -urllib3>=1.26.5 \ No newline at end of file +urllib3>=1.26.0 diff --git a/setup.py b/setup.py index 0ffc5aa..8f3b7fc 100644 --- a/setup.py +++ b/setup.py @@ -1,31 +1,56 @@ # -*- coding: utf-8 -*- - +import re from setuptools import setup with open("README.md", "r") as fh: long_description = fh.read() +with open("requirements.txt", "r") as fh: + reqs = fh.read().split("\n") + +with open("dev-requirements.txt", "r") as fh: + dev_reqs = fh.read().split("\n") + +with open("docs-requirements.txt", "r") as fh: + docs_reqs = fh.read().split("\n") + + +VERSIONFILE = "keycloak/_version.py" +verstrline = open(VERSIONFILE, "rt").read() +VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" +mo = re.search(VSRE, verstrline, re.M) +if mo: + verstr = mo.group(1) +else: + raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,)) + setup( - name='python-keycloak', - version='0.27.1', - url='https://github.com/marcospereirampj/python-keycloak', - license='The MIT License', - author='Marcos Pereira', - author_email='marcospereira.mpj@gmail.com', - keywords='keycloak openid', - description='python-keycloak is a Python package providing access to the Keycloak API.', + name="python-keycloak", + version=verstr, + url="https://github.com/marcospereirampj/python-keycloak", + license="The MIT License", + author="Marcos Pereira, Richard Nemeth", + author_email="marcospereira.mpj@gmail.com; ryshoooo@gmail.com", + keywords="keycloak openid oidc", + description="python-keycloak is a Python package providing access to the Keycloak API.", long_description=long_description, long_description_content_type="text/markdown", - packages=['keycloak', 'keycloak.authorization', 'keycloak.tests'], - install_requires=['requests>=2.20.0', 'python-jose>=1.4.0'], - tests_require=['httmock>=1.2.5'], + packages=["keycloak"], + install_requires=reqs, + tests_require=dev_reqs, + extras_require={"docs": docs_reqs}, + python_requires=">=3.7", + project_urls={ + "Documentation": "https://python-keycloak.readthedocs.io/en/latest/", + "Issue tracker": "https://github.com/marcospereirampj/python-keycloak/issues", + }, classifiers=[ - 'Programming Language :: Python :: 3', - 'License :: OSI Approved :: MIT License', - 'Development Status :: 3 - Alpha', - 'Operating System :: MacOS', - 'Operating System :: Unix', - 'Operating System :: Microsoft :: Windows', - 'Topic :: Utilities' - ] + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Development Status :: 3 - Alpha", + "Operating System :: MacOS", + "Operating System :: Unix", + "Operating System :: Microsoft :: Windows", + "Topic :: Utilities", + ], ) diff --git a/test_keycloak_init.sh b/test_keycloak_init.sh new file mode 100755 index 0000000..bee8830 --- /dev/null +++ b/test_keycloak_init.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +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 +} + +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 + SECONDS=0 + until curl localhost:$KEYCLOAK_PORT; do + sleep 5; + if [ ${SECONDS} -gt 180 ]; then + echo "Timeout exceeded"; + exit 1; + fi + done +} + +# Ensuring that keycloak is stopped in case of CTRL-C +trap keycloak_stop err exit + +keycloak_stop # In case it did not shut down correctly last time. +keycloak_start + +eval ${CMD_ARGS} +RETURN_VALUE=$? + +exit ${RETURN_VALUE} diff --git a/keycloak/tests/__init__.py b/tests/__init__.py similarity index 100% rename from keycloak/tests/__init__.py rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..b9c266a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,61 @@ +import os +import uuid + +import pytest + +from keycloak import KeycloakAdmin + + +@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): + return KeycloakAdmin( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + username=env.KEYCLOAK_ADMIN, + password=env.KEYCLOAK_ADMIN_PASSWORD, + ) + + +@pytest.fixture +def realm(admin: KeycloakAdmin) -> str: + realm_name = str(uuid.uuid4()) + admin.create_realm(payload={"realm": realm_name}) + yield realm_name + admin.delete_realm(realm_name=realm_name) + + +@pytest.fixture +def user(admin: KeycloakAdmin, realm: str) -> str: + admin.realm_name = realm + username = str(uuid.uuid4()) + user_id = admin.create_user(payload={"username": username, "email": f"{username}@test.test"}) + yield user_id + admin.delete_user(user_id=user_id) + + +@pytest.fixture +def group(admin: KeycloakAdmin, realm: str) -> str: + admin.realm_name = realm + group_name = str(uuid.uuid4()) + group_id = admin.create_group(payload={"name": group_name}) + yield group_id + admin.delete_group(group_id=group_id) + + +@pytest.fixture +def client(admin: KeycloakAdmin, realm: str) -> str: + admin.realm_name = realm + client = str(uuid.uuid4()) + client_id = admin.create_client(payload={"name": client, "clientId": client}) + yield client_id + admin.delete_client(client_id=client_id) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py new file mode 100644 index 0000000..29d3b15 --- /dev/null +++ b/tests/test_keycloak_admin.py @@ -0,0 +1,1201 @@ +import pytest + +import keycloak +from keycloak import KeycloakAdmin +from keycloak.connection import ConnectionManager +from keycloak.exceptions import ( + KeycloakDeleteError, + KeycloakGetError, + KeycloakPostError, + KeycloakPutError, +) + + +def test_keycloak_version(): + assert keycloak.__version__, keycloak.__version__ + + +def test_keycloak_admin_bad_init(env): + with pytest.raises(TypeError) as err: + KeycloakAdmin( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + username=env.KEYCLOAK_ADMIN, + password=env.KEYCLOAK_ADMIN_PASSWORD, + auto_refresh_token=1, + ) + assert err.match("Expected a list of strings") + + with pytest.raises(TypeError) as err: + KeycloakAdmin( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + username=env.KEYCLOAK_ADMIN, + password=env.KEYCLOAK_ADMIN_PASSWORD, + auto_refresh_token=["patch"], + ) + assert err.match("Unexpected method in auto_refresh_token") + + +def test_keycloak_admin_init(env): + admin = KeycloakAdmin( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + username=env.KEYCLOAK_ADMIN, + password=env.KEYCLOAK_ADMIN_PASSWORD, + ) + assert admin.server_url == f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", admin.server_url + assert admin.realm_name == "master", admin.realm_name + assert isinstance(admin.connection, ConnectionManager), type(admin.connection) + assert admin.client_id == "admin-cli", admin.client_id + assert admin.client_secret_key is None, admin.client_secret_key + assert admin.verify, admin.verify + assert admin.username == env.KEYCLOAK_ADMIN, admin.username + assert admin.password == env.KEYCLOAK_ADMIN_PASSWORD, admin.password + assert admin.totp is None, admin.totp + assert admin.token is not None, admin.token + assert admin.auto_refresh_token == list(), admin.auto_refresh_token + assert admin.user_realm_name is None, admin.user_realm_name + assert admin.custom_headers is None, admin.custom_headers + + +def test_realms(admin: KeycloakAdmin): + # Get realms + realms = admin.get_realms() + assert len(realms) == 1, realms + assert "master" == realms[0]["realm"] + + # Create a test realm + res = admin.create_realm(payload={"realm": "test"}) + assert res == b"", res + + # Create the same realm, should fail + with pytest.raises(KeycloakPostError) as err: + res = admin.create_realm(payload={"realm": "test"}) + assert err.match('409: b\'{"errorMessage":"Conflict detected. See logs for details"}\'') + + # Create the same realm, skip_exists true + res = admin.create_realm(payload={"realm": "test"}, skip_exists=True) + assert res == {"msg": "Already exists"}, res + + # Get a single realm + res = admin.get_realm(realm_name="test") + assert res["realm"] == "test" + + # Get non-existing realm + with pytest.raises(KeycloakGetError) as err: + admin.get_realm(realm_name="non-existent") + assert err.match('404: b\'{"error":"Realm not found."}\'') + + # Update realm + res = admin.update_realm(realm_name="test", payload={"accountTheme": "test"}) + assert res == dict(), res + + # Check that the update worked + res = admin.get_realm(realm_name="test") + assert res["realm"] == "test" + assert res["accountTheme"] == "test" + + # Update wrong payload + with pytest.raises(KeycloakPutError) as err: + admin.update_realm(realm_name="test", payload={"wrong": "payload"}) + assert err.match('400: b\'{"error":"Unrecognized field') + + # Check that get realms returns both realms + realms = admin.get_realms() + realm_names = [x["realm"] for x in realms] + assert len(realms) == 2, realms + assert "master" in realm_names, realm_names + assert "test" in realm_names, realm_names + + # Delete the realm + res = admin.delete_realm(realm_name="test") + assert res == dict(), res + + # Check that the realm does not exist anymore + with pytest.raises(KeycloakGetError) as err: + admin.get_realm(realm_name="test") + assert err.match('404: b\'{"error":"Realm not found."}\'') + + # Delete non-existing realm + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_realm(realm_name="non-existent") + assert err.match('404: b\'{"error":"Realm not found."}\'') + + +def test_import_export_realms(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + realm_export = admin.export_realm(export_clients=True, export_groups_and_role=True) + assert realm_export != dict(), realm_export + + admin.delete_realm(realm_name=realm) + admin.realm_name = "master" + res = admin.import_realm(payload=realm_export) + assert res == b"", res + + # Test bad import + with pytest.raises(KeycloakPostError) as err: + admin.import_realm(payload=dict()) + assert err.match('500: b\'{"error":"unknown_error"}\'') + + +def test_users(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Check no users present + users = admin.get_users() + assert users == list(), users + + # Test create user + user_id = admin.create_user(payload={"username": "test", "email": "test@test.test"}) + assert user_id is not None, user_id + + # Test create the same user + with pytest.raises(KeycloakPostError) as err: + admin.create_user(payload={"username": "test", "email": "test@test.test"}) + assert err.match('409: b\'{"errorMessage":"User exists with same username"}\'') + + # Test create the same user, exists_ok true + user_id_2 = admin.create_user( + payload={"username": "test", "email": "test@test.test"}, exist_ok=True + ) + assert user_id == user_id_2 + + # Test get user + user = admin.get_user(user_id=user_id) + assert user["username"] == "test", user["username"] + assert user["email"] == "test@test.test", user["email"] + + # Test update user + res = admin.update_user(user_id=user_id, payload={"firstName": "Test"}) + assert res == dict(), res + user = admin.get_user(user_id=user_id) + assert user["firstName"] == "Test" + + # Test update user fail + with pytest.raises(KeycloakPutError) as err: + admin.update_user(user_id=user_id, payload={"wrong": "payload"}) + assert err.match('400: b\'{"error":"Unrecognized field') + + # Test get users again + users = admin.get_users() + usernames = [x["username"] for x in users] + assert "test" in usernames + + # Test users counts + count = admin.users_count() + assert count == 1, count + + # Test user groups + groups = admin.get_user_groups(user_id=user["id"]) + assert len(groups) == 0 + + # Test user groups bad id + with pytest.raises(KeycloakGetError) as err: + admin.get_user_groups(user_id="does-not-exist") + assert err.match('404: b\'{"error":"User not found"}\'') + + # Test logout + res = admin.user_logout(user_id=user["id"]) + assert res == dict(), res + + # Test logout fail + with pytest.raises(KeycloakPostError) as err: + admin.user_logout(user_id="non-existent-id") + assert err.match('404: b\'{"error":"User not found"}\'') + + # Test consents + res = admin.user_consents(user_id=user["id"]) + assert len(res) == 0, res + + # Test consents fail + with pytest.raises(KeycloakGetError) as err: + admin.user_consents(user_id="non-existent-id") + assert err.match('404: b\'{"error":"User not found"}\'') + + # Test delete user + res = admin.delete_user(user_id=user_id) + assert res == dict(), res + with pytest.raises(KeycloakGetError) as err: + admin.get_user(user_id=user_id) + err.match('404: b\'{"error":"User not found"}\'') + + # Test delete fail + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_user(user_id="non-existent-id") + assert err.match('404: b\'{"error":"User not found"}\'') + + +def test_users_pagination(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + for ind in range(admin.PAGE_SIZE + 50): + username = f"user_{ind}" + admin.create_user(payload={"username": username, "email": f"{username}@test.test"}) + + users = admin.get_users() + assert len(users) == admin.PAGE_SIZE + 50, len(users) + + users = admin.get_users(query={"first": 100}) + assert len(users) == 50, len(users) + + users = admin.get_users(query={"max": 20}) + assert len(users) == 20, len(users) + + +def test_idps(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Create IDP + res = admin.create_idp( + payload=dict( + providerId="github", alias="github", config=dict(clientId="test", clientSecret="test") + ) + ) + assert res == b"", res + + # Test create idp fail + with pytest.raises(KeycloakPostError) as err: + admin.create_idp(payload={"providerId": "does-not-exist", "alias": "something"}) + assert err.match("Invalid identity provider id"), err + + # Test listing + idps = admin.get_idps() + assert len(idps) == 1 + assert "github" == idps[0]["alias"] + + # Test adding a mapper + res = admin.add_mapper_to_idp( + idp_alias="github", + payload={ + "identityProviderAlias": "github", + "identityProviderMapper": "github-user-attribute-mapper", + "name": "test", + }, + ) + assert res == b"", res + + # Test mapper fail + with pytest.raises(KeycloakPostError) as err: + admin.add_mapper_to_idp(idp_alias="does-no-texist", payload=dict()) + assert err.match('404: b\'{"error":"HTTP 404 Not Found"}\'') + + # Test delete + res = admin.delete_idp(idp_alias="github") + assert res == dict(), res + + # Test delete fail + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_idp(idp_alias="does-not-exist") + assert err.match('404: b\'{"error":"HTTP 404 Not Found"}\'') + + +def test_user_credentials(admin: KeycloakAdmin, user: str): + res = admin.set_user_password(user_id=user, password="booya", temporary=True) + assert res == dict(), res + + # Test user password set fail + with pytest.raises(KeycloakPutError) as err: + admin.set_user_password(user_id="does-not-exist", password="") + assert err.match('404: b\'{"error":"User not found"}\'') + + credentials = admin.get_credentials(user_id=user) + assert len(credentials) == 1 + assert credentials[0]["type"] == "password", credentials + + # Test get credentials fail + with pytest.raises(KeycloakGetError) as err: + admin.get_credentials(user_id="does-not-exist") + assert err.match('404: b\'{"error":"User not found"}\'') + + res = admin.delete_credential(user_id=user, credential_id=credentials[0]["id"]) + assert res == dict(), res + + # Test delete fail + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_credential(user_id=user, credential_id="does-not-exist") + assert err.match('404: b\'{"error":"Credential not found"}\'') + + +def test_social_logins(admin: KeycloakAdmin, user: str): + res = admin.add_user_social_login( + user_id=user, provider_id="gitlab", provider_userid="test", provider_username="test" + ) + assert res == dict(), res + admin.add_user_social_login( + user_id=user, provider_id="github", provider_userid="test", provider_username="test" + ) + assert res == dict(), res + + # Test add social login fail + with pytest.raises(KeycloakPostError) as err: + admin.add_user_social_login( + user_id="does-not-exist", + provider_id="does-not-exist", + provider_userid="test", + provider_username="test", + ) + assert err.match('404: b\'{"error":"User not found"}\'') + + res = admin.get_user_social_logins(user_id=user) + assert res == list(), res + + # Test get social logins fail + with pytest.raises(KeycloakGetError) as err: + admin.get_user_social_logins(user_id="does-not-exist") + assert err.match('404: b\'{"error":"User not found"}\'') + + res = admin.delete_user_social_login(user_id=user, provider_id="gitlab") + assert res == {}, res + + res = admin.delete_user_social_login(user_id=user, provider_id="github") + assert res == {}, res + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_user_social_login(user_id=user, provider_id="instagram") + assert err.match('404: b\'{"error":"Link not found"}\''), err + + +def test_server_info(admin: KeycloakAdmin): + info = admin.get_server_info() + assert set(info.keys()) == { + "systemInfo", + "memoryInfo", + "profileInfo", + "themes", + "socialProviders", + "identityProviders", + "providers", + "protocolMapperTypes", + "builtinProtocolMappers", + "clientInstallations", + "componentTypes", + "passwordPolicies", + "enums", + }, info.keys() + + +def test_groups(admin: KeycloakAdmin, user: str): + # Test get groups + groups = admin.get_groups() + assert len(groups) == 0 + + # Test create group + group_id = admin.create_group(payload={"name": "main-group"}) + assert group_id is not None, group_id + + # Test create subgroups + subgroup_id_1 = admin.create_group(payload={"name": "subgroup-1"}, parent=group_id) + subgroup_id_2 = admin.create_group(payload={"name": "subgroup-2"}, parent=group_id) + + # Test create group fail + with pytest.raises(KeycloakPostError) as err: + admin.create_group(payload={"name": "subgroup-1"}, parent=group_id) + assert err.match('409: b\'{"error":"unknown_error"}\''), err + + # Test skip exists OK + subgroup_id_1_eq = admin.create_group( + payload={"name": "subgroup-1"}, parent=group_id, skip_exists=True + ) + assert subgroup_id_1_eq is None + + # Test get groups again + groups = admin.get_groups() + assert len(groups) == 1, groups + assert len(groups[0]["subGroups"]) == 2, groups["subGroups"] + assert groups[0]["id"] == group_id + assert {x["id"] for x in groups[0]["subGroups"]} == {subgroup_id_1, subgroup_id_2} + + # Test get groups query + groups = admin.get_groups(query={"max": 10}) + assert len(groups) == 1, groups + assert len(groups[0]["subGroups"]) == 2, groups["subGroups"] + assert groups[0]["id"] == group_id + assert {x["id"] for x in groups[0]["subGroups"]} == {subgroup_id_1, subgroup_id_2} + + # Test get group + res = admin.get_group(group_id=subgroup_id_1) + assert res["id"] == subgroup_id_1, res + assert res["name"] == "subgroup-1" + assert res["path"] == "/main-group/subgroup-1" + + # Test get group fail + with pytest.raises(KeycloakGetError) as err: + admin.get_group(group_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find group by id"}\''), err + + # Create 1 more subgroup + subsubgroup_id_1 = admin.create_group(payload={"name": "subsubgroup-1"}, parent=subgroup_id_2) + main_group = admin.get_group(group_id=group_id) + + # Test nested searches + res = admin.get_subgroups(group=main_group, path="/main-group/subgroup-2/subsubgroup-1") + assert res is not None, res + assert res["id"] == subsubgroup_id_1 + + # Test empty search + res = admin.get_subgroups(group=main_group, path="/none") + assert res is None, res + + # Test get group by path + res = admin.get_group_by_path(path="/main-group/subgroup-1") + assert res is None, res + + res = admin.get_group_by_path(path="/main-group/subgroup-1", search_in_subgroups=True) + assert res is not None, res + assert res["id"] == subgroup_id_1, res + + res = admin.get_group_by_path( + path="/main-group/subgroup-2/subsubgroup-1/test", search_in_subgroups=True + ) + assert res is None, res + + res = admin.get_group_by_path( + path="/main-group/subgroup-2/subsubgroup-1", search_in_subgroups=True + ) + assert res is not None, res + assert res["id"] == subsubgroup_id_1 + + res = admin.get_group_by_path(path="/main-group") + assert res is not None, res + assert res["id"] == group_id, res + + # Test group members + res = admin.get_group_members(group_id=subgroup_id_2) + assert len(res) == 0, res + + # Test fail group members + with pytest.raises(KeycloakGetError) as err: + admin.get_group_members(group_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find group by id"}\'') + + res = admin.group_user_add(user_id=user, group_id=subgroup_id_2) + assert res == dict(), res + + res = admin.get_group_members(group_id=subgroup_id_2) + assert len(res) == 1, res + assert res[0]["id"] == user + + # Test get group members query + res = admin.get_group_members(group_id=subgroup_id_2, query={"max": 10}) + assert len(res) == 1, res + assert res[0]["id"] == user + + with pytest.raises(KeycloakDeleteError) as err: + admin.group_user_remove(user_id="does-not-exist", group_id=subgroup_id_2) + assert err.match('404: b\'{"error":"User not found"}\''), err + + res = admin.group_user_remove(user_id=user, group_id=subgroup_id_2) + assert res == dict(), res + + # Test set permissions + res = admin.group_set_permissions(group_id=subgroup_id_2, enabled=True) + assert res["enabled"], res + res = admin.group_set_permissions(group_id=subgroup_id_2, enabled=False) + assert not res["enabled"], res + with pytest.raises(KeycloakPutError) as err: + admin.group_set_permissions(group_id=subgroup_id_2, enabled="blah") + assert err.match('500: b\'{"error":"unknown_error"}\''), err + + # Test update group + res = admin.update_group(group_id=subgroup_id_2, payload={"name": "new-subgroup-2"}) + assert res == dict(), res + assert admin.get_group(group_id=subgroup_id_2)["name"] == "new-subgroup-2" + + # test update fail + with pytest.raises(KeycloakPutError) as err: + admin.update_group(group_id="does-not-exist", payload=dict()) + assert err.match('404: b\'{"error":"Could not find group by id"}\''), err + + # Test delete + res = admin.delete_group(group_id=group_id) + assert res == dict(), res + assert len(admin.get_groups()) == 0 + + # Test delete fail + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_group(group_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find group by id"}\''), err + + +def test_clients(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Test get clients + clients = admin.get_clients() + assert len(clients) == 6, clients + assert {x["name"] for x in clients} == set( + [ + "${client_admin-cli}", + "${client_security-admin-console}", + "${client_account-console}", + "${client_broker}", + "${client_account}", + "${client_realm-management}", + ] + ), clients + + # Test create client + client_id = admin.create_client(payload={"name": "test-client", "clientId": "test-client"}) + assert client_id, client_id + + with pytest.raises(KeycloakPostError) as err: + admin.create_client(payload={"name": "test-client", "clientId": "test-client"}) + assert err.match('409: b\'{"errorMessage":"Client test-client already exists"}\''), err + + client_id_2 = admin.create_client( + payload={"name": "test-client", "clientId": "test-client"}, skip_exists=True + ) + assert client_id == client_id_2, client_id_2 + + # Test get client + res = admin.get_client(client_id=client_id) + assert res["clientId"] == "test-client", res + assert res["name"] == "test-client", res + assert res["id"] == client_id, res + + with pytest.raises(KeycloakGetError) as err: + admin.get_client(client_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find client"}\'') + assert len(admin.get_clients()) == 7 + + # Test get client id + assert admin.get_client_id(client_name="test-client") == client_id + assert admin.get_client_id(client_name="does-not-exist") is None + + # Test update client + res = admin.update_client(client_id=client_id, payload={"name": "test-client-change"}) + assert res == dict(), res + + with pytest.raises(KeycloakPutError) as err: + admin.update_client(client_id="does-not-exist", payload={"name": "test-client-change"}) + assert err.match('404: b\'{"error":"Could not find client"}\'') + + # Test authz + auth_client_id = admin.create_client( + payload={ + "name": "authz-client", + "clientId": "authz-client", + "authorizationServicesEnabled": True, + "serviceAccountsEnabled": True, + } + ) + res = admin.get_client_authz_settings(client_id=auth_client_id) + assert res["allowRemoteResourceManagement"] + assert res["decisionStrategy"] == "UNANIMOUS" + assert len(res["policies"]) >= 0 + + with pytest.raises(KeycloakGetError) as err: + admin.get_client_authz_settings(client_id=client_id) + assert err.match('500: b\'{"error":"HTTP 500 Internal Server Error"}\'') + + # Authz resources + res = admin.get_client_authz_resources(client_id=auth_client_id) + assert len(res) == 1 + assert res[0]["name"] == "Default Resource" + + with pytest.raises(KeycloakGetError) as err: + admin.get_client_authz_resources(client_id=client_id) + assert err.match('500: b\'{"error":"unknown_error"}\'') + + res = admin.create_client_authz_resource( + client_id=auth_client_id, payload={"name": "test-resource"} + ) + assert res["name"] == "test-resource", res + test_resource_id = res["_id"] + + with pytest.raises(KeycloakPostError) as err: + admin.create_client_authz_resource( + client_id=auth_client_id, payload={"name": "test-resource"} + ) + assert err.match('409: b\'{"error":"invalid_request"') + assert admin.create_client_authz_resource( + client_id=auth_client_id, payload={"name": "test-resource"}, skip_exists=True + ) == {"msg": "Already exists"} + + res = admin.get_client_authz_resources(client_id=auth_client_id) + assert len(res) == 2 + assert {x["name"] for x in res} == {"Default Resource", "test-resource"} + + # Authz policies + res = admin.get_client_authz_policies(client_id=auth_client_id) + assert len(res) == 1, res + assert res[0]["name"] == "Default Policy" + assert len(admin.get_client_authz_policies(client_id=client_id)) == 1 + + with pytest.raises(KeycloakGetError) as err: + admin.get_client_authz_policies(client_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find client"}\'') + + role_id = admin.get_realm_role(role_name="offline_access")["id"] + res = admin.create_client_authz_role_based_policy( + client_id=auth_client_id, + payload={"name": "test-authz-rb-policy", "roles": [{"id": role_id}]}, + ) + assert res["name"] == "test-authz-rb-policy", res + + with pytest.raises(KeycloakPostError) as err: + admin.create_client_authz_role_based_policy( + client_id=auth_client_id, + payload={"name": "test-authz-rb-policy", "roles": [{"id": role_id}]}, + ) + assert err.match('409: b\'{"error":"Policy with name') + assert admin.create_client_authz_role_based_policy( + client_id=auth_client_id, + payload={"name": "test-authz-rb-policy", "roles": [{"id": role_id}]}, + skip_exists=True, + ) == {"msg": "Already exists"} + assert len(admin.get_client_authz_policies(client_id=auth_client_id)) == 2 + + # Test authz permissions + res = admin.get_client_authz_permissions(client_id=auth_client_id) + assert len(res) == 1, res + assert res[0]["name"] == "Default Permission" + assert len(admin.get_client_authz_permissions(client_id=client_id)) == 1 + + with pytest.raises(KeycloakGetError) as err: + admin.get_client_authz_permissions(client_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find client"}\'') + + res = admin.create_client_authz_resource_based_permission( + client_id=auth_client_id, + payload={"name": "test-permission-rb", "resources": [test_resource_id]}, + ) + assert res, res + assert res["name"] == "test-permission-rb" + assert res["resources"] == [test_resource_id] + + with pytest.raises(KeycloakPostError) as err: + admin.create_client_authz_resource_based_permission( + client_id=auth_client_id, + payload={"name": "test-permission-rb", "resources": [test_resource_id]}, + ) + assert err.match('409: b\'{"error":"Policy with name') + assert admin.create_client_authz_resource_based_permission( + client_id=auth_client_id, + payload={"name": "test-permission-rb", "resources": [test_resource_id]}, + skip_exists=True, + ) == {"msg": "Already exists"} + assert len(admin.get_client_authz_permissions(client_id=auth_client_id)) == 2 + + # Test authz scopes + res = admin.get_client_authz_scopes(client_id=auth_client_id) + assert len(res) == 0, res + + with pytest.raises(KeycloakGetError) as err: + admin.get_client_authz_scopes(client_id=client_id) + assert err.match('500: b\'{"error":"unknown_error"}\'') + + # Test service account user + res = admin.get_client_service_account_user(client_id=auth_client_id) + assert res["username"] == "service-account-authz-client", res + + with pytest.raises(KeycloakGetError) as err: + admin.get_client_service_account_user(client_id=client_id) + assert err.match('400: b\'{"error":"unknown_error"}\'') + + # Test delete client + res = admin.delete_client(client_id=auth_client_id) + assert res == dict(), res + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_client(client_id=auth_client_id) + assert err.match('404: b\'{"error":"Could not find client"}\'') + + +def test_realm_roles(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Test get realm roles + roles = admin.get_realm_roles() + assert len(roles) == 3, roles + role_names = [x["name"] for x in roles] + assert "uma_authorization" in role_names, role_names + assert "offline_access" in role_names, role_names + + # Test empty members + with pytest.raises(KeycloakGetError) as err: + admin.get_realm_role_members(role_name="does-not-exist") + assert err.match('404: b\'{"error":"Could not find role"}\'') + members = admin.get_realm_role_members(role_name="offline_access") + assert members == list(), members + + # Test create realm role + role_id = admin.create_realm_role(payload={"name": "test-realm-role"}) + assert role_id, role_id + with pytest.raises(KeycloakPostError) as err: + admin.create_realm_role(payload={"name": "test-realm-role"}) + assert err.match('409: b\'{"errorMessage":"Role with name test-realm-role already exists"}\'') + role_id_2 = admin.create_realm_role(payload={"name": "test-realm-role"}, skip_exists=True) + assert role_id == role_id_2 + + # Test update realm role + res = admin.update_realm_role( + role_name="test-realm-role", payload={"name": "test-realm-role-update"} + ) + assert res == dict(), res + with pytest.raises(KeycloakPutError) as err: + admin.update_realm_role( + role_name="test-realm-role", payload={"name": "test-realm-role-update"} + ) + assert err.match('404: b\'{"error":"Could not find role"}\''), err + + # Test realm role user assignment + user_id = admin.create_user(payload={"username": "role-testing", "email": "test@test.test"}) + with pytest.raises(KeycloakPostError) as err: + admin.assign_realm_roles(user_id=user_id, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.assign_realm_roles( + user_id=user_id, + roles=[ + admin.get_realm_role(role_name="offline_access"), + admin.get_realm_role(role_name="test-realm-role-update"), + ], + ) + assert res == dict(), res + assert admin.get_user(user_id=user_id)["username"] in [ + x["username"] for x in admin.get_realm_role_members(role_name="offline_access") + ] + assert admin.get_user(user_id=user_id)["username"] in [ + x["username"] for x in admin.get_realm_role_members(role_name="test-realm-role-update") + ] + + roles = admin.get_realm_roles_of_user(user_id=user_id) + assert len(roles) == 3 + assert "offline_access" in [x["name"] for x in roles] + assert "test-realm-role-update" in [x["name"] for x in roles] + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_realm_roles_of_user(user_id=user_id, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.delete_realm_roles_of_user( + user_id=user_id, roles=[admin.get_realm_role(role_name="offline_access")] + ) + assert res == dict(), res + assert admin.get_realm_role_members(role_name="offline_access") == list() + roles = admin.get_realm_roles_of_user(user_id=user_id) + assert len(roles) == 2 + assert "offline_access" not in [x["name"] for x in roles] + assert "test-realm-role-update" in [x["name"] for x in roles] + + roles = admin.get_available_realm_roles_of_user(user_id=user_id) + assert len(roles) == 2 + assert "offline_access" in [x["name"] for x in roles] + assert "uma_authorization" in [x["name"] for x in roles] + + # Test realm role group assignment + group_id = admin.create_group(payload={"name": "test-group"}) + with pytest.raises(KeycloakPostError) as err: + admin.assign_group_realm_roles(group_id=group_id, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.assign_group_realm_roles( + group_id=group_id, + roles=[ + admin.get_realm_role(role_name="offline_access"), + admin.get_realm_role(role_name="test-realm-role-update"), + ], + ) + assert res == dict(), res + + roles = admin.get_group_realm_roles(group_id=group_id) + assert len(roles) == 2 + assert "offline_access" in [x["name"] for x in roles] + assert "test-realm-role-update" in [x["name"] for x in roles] + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_group_realm_roles(group_id=group_id, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.delete_group_realm_roles( + group_id=group_id, roles=[admin.get_realm_role(role_name="offline_access")] + ) + assert res == dict(), res + roles = admin.get_group_realm_roles(group_id=group_id) + assert len(roles) == 1 + assert "test-realm-role-update" in [x["name"] for x in roles] + + # Test composite realm roles + composite_role = admin.create_realm_role(payload={"name": "test-composite-role"}) + with pytest.raises(KeycloakPostError) as err: + admin.add_composite_realm_roles_to_role(role_name=composite_role, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.add_composite_realm_roles_to_role( + role_name=composite_role, roles=[admin.get_realm_role(role_name="test-realm-role-update")] + ) + assert res == dict(), res + + res = admin.get_composite_realm_roles_of_role(role_name=composite_role) + assert len(res) == 1 + assert "test-realm-role-update" in res[0]["name"] + with pytest.raises(KeycloakGetError) as err: + admin.get_composite_realm_roles_of_role(role_name="bad") + assert err.match('404: b\'{"error":"Could not find role"}\'') + + res = admin.get_composite_realm_roles_of_user(user_id=user_id) + assert len(res) == 4 + assert "offline_access" in {x["name"] for x in res} + assert "test-realm-role-update" in {x["name"] for x in res} + assert "uma_authorization" in {x["name"] for x in res} + with pytest.raises(KeycloakGetError) as err: + admin.get_composite_realm_roles_of_user(user_id="bad") + assert err.match('404: b\'{"error":"User not found"}\'') + + with pytest.raises(KeycloakDeleteError) as err: + admin.remove_composite_realm_roles_to_role(role_name=composite_role, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.remove_composite_realm_roles_to_role( + role_name=composite_role, roles=[admin.get_realm_role(role_name="test-realm-role-update")] + ) + assert res == dict(), res + + res = admin.get_composite_realm_roles_of_role(role_name=composite_role) + assert len(res) == 0 + + # Test delete realm role + res = admin.delete_realm_role(role_name=composite_role) + assert res == dict(), res + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_realm_role(role_name=composite_role) + assert err.match('404: b\'{"error":"Could not find role"}\'') + + +def test_client_roles(admin: KeycloakAdmin, client: str): + # Test get client roles + res = admin.get_client_roles(client_id=client) + assert len(res) == 0 + with pytest.raises(KeycloakGetError) as err: + admin.get_client_roles(client_id="bad") + assert err.match('404: b\'{"error":"Could not find client"}\'') + + # Test create client role + client_role_id = admin.create_client_role( + client_role_id=client, payload={"name": "client-role-test"} + ) + with pytest.raises(KeycloakPostError) as err: + admin.create_client_role(client_role_id=client, payload={"name": "client-role-test"}) + assert err.match('409: b\'{"errorMessage":"Role with name client-role-test already exists"}\'') + client_role_id_2 = admin.create_client_role( + client_role_id=client, payload={"name": "client-role-test"}, skip_exists=True + ) + assert client_role_id == client_role_id_2 + + # Test get client role + res = admin.get_client_role(client_id=client, role_name="client-role-test") + assert res["name"] == client_role_id + with pytest.raises(KeycloakGetError) as err: + admin.get_client_role(client_id=client, role_name="bad") + assert err.match('404: b\'{"error":"Could not find role"}\'') + + res_ = admin.get_client_role_id(client_id=client, role_name="client-role-test") + assert res_ == res["id"] + with pytest.raises(KeycloakGetError) as err: + admin.get_client_role_id(client_id=client, role_name="bad") + assert err.match('404: b\'{"error":"Could not find role"}\'') + assert len(admin.get_client_roles(client_id=client)) == 1 + + # Test update client role + res = admin.update_client_role( + client_role_id=client, + role_name="client-role-test", + payload={"name": "client-role-test-update"}, + ) + assert res == dict() + with pytest.raises(KeycloakPutError) as err: + res = admin.update_client_role( + client_role_id=client, + role_name="client-role-test", + payload={"name": "client-role-test-update"}, + ) + assert err.match('404: b\'{"error":"Could not find role"}\'') + + # Test user with client role + res = admin.get_client_role_members(client_id=client, role_name="client-role-test-update") + assert len(res) == 0 + with pytest.raises(KeycloakGetError) as err: + admin.get_client_role_members(client_id=client, role_name="bad") + assert err.match('404: b\'{"error":"Could not find role"}\'') + + user_id = admin.create_user(payload={"username": "test", "email": "test@test.test"}) + with pytest.raises(KeycloakPostError) as err: + admin.assign_client_role(user_id=user_id, client_id=client, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.assign_client_role( + user_id=user_id, + client_id=client, + roles=[admin.get_client_role(client_id=client, role_name="client-role-test-update")], + ) + assert res == dict() + assert ( + len(admin.get_client_role_members(client_id=client, role_name="client-role-test-update")) + == 1 + ) + + roles = admin.get_client_roles_of_user(user_id=user_id, client_id=client) + assert len(roles) == 1, roles + with pytest.raises(KeycloakGetError) as err: + admin.get_client_roles_of_user(user_id=user_id, client_id="bad") + assert err.match('404: b\'{"error":"Client not found"}\'') + + roles = admin.get_composite_client_roles_of_user(user_id=user_id, client_id=client) + assert len(roles) == 1, roles + with pytest.raises(KeycloakGetError) as err: + admin.get_composite_client_roles_of_user(user_id=user_id, client_id="bad") + assert err.match('404: b\'{"error":"Client not found"}\'') + + roles = admin.get_available_client_roles_of_user(user_id=user_id, client_id=client) + assert len(roles) == 0, roles + with pytest.raises(KeycloakGetError) as err: + admin.get_composite_client_roles_of_user(user_id=user_id, client_id="bad") + assert err.match('404: b\'{"error":"Client not found"}\'') + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_client_roles_of_user(user_id=user_id, client_id=client, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + admin.delete_client_roles_of_user( + user_id=user_id, + client_id=client, + roles=[admin.get_client_role(client_id=client, role_name="client-role-test-update")], + ) + assert len(admin.get_client_roles_of_user(user_id=user_id, client_id=client)) == 0 + + # Test groups and client roles + res = admin.get_client_role_groups(client_id=client, role_name="client-role-test-update") + assert len(res) == 0 + with pytest.raises(KeycloakGetError) as err: + admin.get_client_role_groups(client_id=client, role_name="bad") + assert err.match('404: b\'{"error":"Could not find role"}\'') + + group_id = admin.create_group(payload={"name": "test-group"}) + res = admin.get_group_client_roles(group_id=group_id, client_id=client) + assert len(res) == 0 + with pytest.raises(KeycloakGetError) as err: + admin.get_group_client_roles(group_id=group_id, client_id="bad") + assert err.match('404: b\'{"error":"Client not found"}\'') + + with pytest.raises(KeycloakPostError) as err: + admin.assign_group_client_roles(group_id=group_id, client_id=client, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.assign_group_client_roles( + group_id=group_id, + client_id=client, + roles=[admin.get_client_role(client_id=client, role_name="client-role-test-update")], + ) + assert res == dict() + assert ( + len(admin.get_client_role_groups(client_id=client, role_name="client-role-test-update")) + == 1 + ) + assert len(admin.get_group_client_roles(group_id=group_id, client_id=client)) == 1 + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_group_client_roles(group_id=group_id, client_id=client, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.delete_group_client_roles( + group_id=group_id, + client_id=client, + roles=[admin.get_client_role(client_id=client, role_name="client-role-test-update")], + ) + assert res == dict() + + # Test composite client roles + with pytest.raises(KeycloakPostError) as err: + admin.add_composite_client_roles_to_role( + client_role_id=client, role_name="client-role-test-update", roles=["bad"] + ) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.add_composite_client_roles_to_role( + client_role_id=client, + role_name="client-role-test-update", + roles=[admin.get_realm_role(role_name="offline_access")], + ) + assert res == dict() + assert admin.get_client_role(client_id=client, role_name="client-role-test-update")[ + "composite" + ] + + # Test delete of client role + res = admin.delete_client_role(client_role_id=client, role_name="client-role-test-update") + assert res == dict() + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_client_role(client_role_id=client, role_name="client-role-test-update") + assert err.match('404: b\'{"error":"Could not find role"}\'') + + +def test_email(admin: KeycloakAdmin, user: str): + # 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()) + assert err.match('500: b\'{"error":"unknown_error"}\'') + + admin.update_user(user_id=user, payload={"enabled": True}) + with pytest.raises(KeycloakPutError) as err: + admin.send_verify_email(user_id=user) + assert err.match('500: b\'{"errorMessage":"Failed to send execute actions email"}\'') + + +def test_get_sessions(admin: KeycloakAdmin): + sessions = admin.get_sessions(user_id=admin.get_user_id(username=admin.username)) + assert len(sessions) >= 1 + with pytest.raises(KeycloakGetError) as err: + admin.get_sessions(user_id="bad") + assert err.match('404: b\'{"error":"User not found"}\'') + + +def test_get_client_installation_provider(admin: KeycloakAdmin, client: str): + 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"}\'') + + installation = admin.get_client_installation_provider( + client_id=client, provider_id="keycloak-oidc-keycloak-json" + ) + assert set(installation.keys()) == { + "auth-server-url", + "confidential-port", + "credentials", + "realm", + "resource", + "ssl-required", + } + + +def test_auth_flows(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + res = admin.get_authentication_flows() + assert len(res) == 8, res + assert set(res[0].keys()) == { + "alias", + "authenticationExecutions", + "builtIn", + "description", + "id", + "providerId", + "topLevel", + } + assert {x["alias"] for x in res} == { + "reset credentials", + "browser", + "http challenge", + "registration", + "docker auth", + "direct grant", + "first broker login", + "clients", + } + + with pytest.raises(KeycloakGetError) as err: + admin.get_authentication_flow_for_id(flow_id="bad") + assert err.match('404: b\'{"error":"Could not find flow with id"}\'') + browser_flow_id = [x for x in res if x["alias"] == "browser"][0]["id"] + res = admin.get_authentication_flow_for_id(flow_id=browser_flow_id) + assert res["alias"] == "browser" + + # Test copying + with pytest.raises(KeycloakPostError) as err: + admin.copy_authentication_flow(payload=dict(), flow_alias="bad") + assert err.match("404: b''") + + res = admin.copy_authentication_flow(payload={"newName": "test-browser"}, flow_alias="browser") + assert res == b"", res + assert len(admin.get_authentication_flows()) == 9 + + # Test create + res = admin.create_authentication_flow( + payload={"alias": "test-create", "providerId": "basic-flow"} + ) + assert res == b"" + with pytest.raises(KeycloakPostError) as err: + admin.create_authentication_flow(payload={"alias": "test-create", "builtIn": False}) + assert err.match('409: b\'{"errorMessage":"Flow test-create already exists"}\'') + assert admin.create_authentication_flow( + payload={"alias": "test-create"}, skip_exists=True + ) == {"msg": "Already exists"} + + # Test flow executions + res = admin.get_authentication_flow_executions(flow_alias="browser") + assert len(res) == 8, res + with pytest.raises(KeycloakGetError) as err: + admin.get_authentication_flow_executions(flow_alias="bad") + assert err.match("404: b''") + exec_id = res[0]["id"] + + res = admin.get_authentication_flow_execution(execution_id=exec_id) + assert set(res.keys()) == { + "alternative", + "authenticator", + "authenticatorFlow", + "conditional", + "disabled", + "enabled", + "id", + "parentFlow", + "priority", + "required", + "requirement", + }, res + with pytest.raises(KeycloakGetError) as err: + admin.get_authentication_flow_execution(execution_id="bad") + assert err.match('404: b\'{"error":"Illegal execution"}\'') + + with pytest.raises(KeycloakPostError) as err: + admin.create_authentication_flow_execution(payload=dict(), flow_alias="browser") + assert err.match('400: b\'{"error":"It is illegal to add execution to a built in flow"}\'') + + res = admin.create_authentication_flow_execution( + payload={"provider": "auth-cookie"}, flow_alias="test-create" + ) + assert res == b"" + assert len(admin.get_authentication_flow_executions(flow_alias="test-create")) == 1 + + with pytest.raises(KeycloakPutError) as err: + admin.update_authentication_flow_executions( + payload={"required": "yes"}, flow_alias="test-create" + ) + assert err.match('400: b\'{"error":"Unrecognized field') + payload = admin.get_authentication_flow_executions(flow_alias="test-create")[0] + payload["displayName"] = "test" + res = admin.update_authentication_flow_executions(payload=payload, flow_alias="test-create") + assert res + + exec_id = admin.get_authentication_flow_executions(flow_alias="test-create")[0]["id"] + res = admin.delete_authentication_flow_execution(execution_id=exec_id) + assert res == dict() + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_authentication_flow_execution(execution_id=exec_id) + assert err.match('404: b\'{"error":"Illegal execution"}\'') + + # Test subflows + res = admin.create_authentication_flow_subflow( + payload={ + "alias": "test-subflow", + "provider": "basic-flow", + "type": "something", + "description": "something", + }, + flow_alias="test-browser", + ) + assert res == b"" + with pytest.raises(KeycloakPostError) as err: + admin.create_authentication_flow_subflow( + payload={"alias": "test-subflow", "providerId": "basic-flow"}, + flow_alias="test-browser", + ) + assert err.match('409: b\'{"errorMessage":"New flow alias name already exists"}\'') + res = admin.create_authentication_flow_subflow( + payload={ + "alias": "test-subflow", + "provider": "basic-flow", + "type": "something", + "description": "something", + }, + flow_alias="test-create", + skip_exists=True, + ) + assert res == {"msg": "Already exists"} + + # Test delete auth flow + flow_id = [x for x in admin.get_authentication_flows() if x["alias"] == "test-browser"][0][ + "id" + ] + res = admin.delete_authentication_flow(flow_id=flow_id) + assert res == dict() + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_authentication_flow(flow_id=flow_id) + assert err.match('404: b\'{"error":"Could not find flow with id"}\'') diff --git a/tests/test_urls_patterns.py b/tests/test_urls_patterns.py new file mode 100644 index 0000000..6fa5a87 --- /dev/null +++ b/tests/test_urls_patterns.py @@ -0,0 +1,26 @@ +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 + + # Test that all patterns start with URL_ + for url in urls: + assert url.startswith("URL_"), f"The url pattern {url} does not begin with URL_" + + # Test that the patterns have unique names + seen_urls = list() + for url in urls: + assert url not in seen_urls, f"The url pattern {url} is present twice." + seen_urls.append(url) + + # Test that the pattern values are unique + seen_url_values = list() + for url in urls: + url_value = urls_patterns.__dict__[url] + assert url_value not in seen_url_values, f"The url {url} has a duplicate value {url_value}" + seen_url_values.append(url_value) diff --git a/tox.env b/tox.env new file mode 100644 index 0000000..49cea83 --- /dev/null +++ b/tox.env @@ -0,0 +1,4 @@ +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin +KEYCLOAK_HOST={env:KEYCLOAK_HOST:localhost} +KEYCLOAK_PORT=8080 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..2726f66 --- /dev/null +++ b/tox.ini @@ -0,0 +1,48 @@ +[tox] +envlist = check, apply-check, docs, tests, build + +[testenv] +install_command = pip install {opts} {packages} + +[testenv:check] +deps = + black + isort + flake8 +commands = + black --check --diff keycloak tests docs + isort -c --df keycloak tests docs + flake8 keycloak tests docs + +[testenv:apply-check] +deps = + black + isort + flake8 +commands = + black -C keycloak tests docs + black keycloak tests docs + isort keycloak tests docs + +[testenv:docs] +deps = + .[docs] +commands = + python -m sphinx -T -E -W -b html -d _build/doctrees -D language=en ./docs/source _build/html + +[testenv:tests] +setenv = file|tox.env +deps = + -rrequirements.txt + -rdev-requirements.txt +commands = + ./test_keycloak_init.sh "pytest -vv --cov=keycloak --cov-report term-missing {posargs}" + +[testenv:build] +deps = + -rdev-requirements.txt +commands = + python setup.py sdist bdist_wheel + +[flake8] +max-line-length = 99 From 8d19ea8180b494832d36126615bbcb9c5c9de159 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 12:06:04 +0200 Subject: [PATCH 2/9] fix: raise correct errors --- keycloak/__init__.py | 7 +- keycloak/keycloak_admin.py | 164 +++++++++++++++++++------------------ 2 files changed, 89 insertions(+), 82 deletions(-) diff --git a/keycloak/__init__.py b/keycloak/__init__.py index 987ce1c..62c47a8 100644 --- a/keycloak/__init__.py +++ b/keycloak/__init__.py @@ -21,5 +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. -from .keycloak_admin import * -from .keycloak_openid import * +from ._version import __version__ +from .keycloak_admin import KeycloakAdmin +from .keycloak_openid import KeycloakOpenID + +__all__ = ["KeycloakAdmin", "KeycloakOpenID", "__version__"] diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 5725a1a..3e30722 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -29,7 +29,13 @@ from builtins import isinstance from typing import Iterable from .connection import ConnectionManager -from .exceptions import KeycloakGetError, raise_error_from_response +from .exceptions import ( + KeycloakDeleteError, + KeycloakGetError, + KeycloakPostError, + KeycloakPutError, + raise_error_from_response, +) from .keycloak_openid import KeycloakOpenID from .urls_patterns import ( URL_ADMIN_AUTHENTICATOR_CONFIG, @@ -341,7 +347,7 @@ class KeycloakAdmin: """ data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def export_realm(self, export_clients=False, export_groups_and_role=False): """ @@ -361,7 +367,7 @@ class KeycloakAdmin: "export-groups-and-roles": export_groups_and_role, } data_raw = self.raw_post(URL_ADMIN_REALM_EXPORT.format(**params_path), data="") - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def get_realms(self): """ @@ -386,7 +392,7 @@ class KeycloakAdmin: data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def update_realm(self, realm_name, payload): @@ -404,7 +410,7 @@ class KeycloakAdmin: params_path = {"realm-name": realm_name} data_raw = self.raw_put(URL_ADMIN_REALM.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_realm(self, realm_name): """ @@ -416,7 +422,7 @@ class KeycloakAdmin: params_path = {"realm-name": realm_name} data_raw = self.raw_delete(URL_ADMIN_REALM.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_users(self, query=None): """ @@ -448,7 +454,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_IDPS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def add_mapper_to_idp(self, idp_alias, payload): """ @@ -464,7 +470,7 @@ class KeycloakAdmin: data_raw = self.raw_post( URL_ADMIN_IDP_MAPPERS.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def get_idps(self): """ @@ -487,7 +493,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "alias": idp_alias} data_raw = self.raw_delete(URL_ADMIN_IDP.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def create_user(self, payload, exist_ok=True): """ @@ -510,7 +516,7 @@ class KeycloakAdmin: return str(exists) data_raw = self.raw_post(URL_ADMIN_USERS.format(**params_path), data=json.dumps(payload)) - raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) _last_slash_idx = data_raw.headers["Location"].rindex("/") return data_raw.headers["Location"][_last_slash_idx + 1 :] @@ -578,7 +584,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_put(URL_ADMIN_USER.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_user(self, user_id): """ @@ -590,7 +596,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_delete(URL_ADMIN_USER.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def set_user_password(self, user_id, password, temporary=True): """ @@ -611,7 +617,7 @@ class KeycloakAdmin: data_raw = self.raw_put( URL_ADMIN_RESET_PASSWORD.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_credentials(self, user_id): """ @@ -663,7 +669,7 @@ class KeycloakAdmin: "credential_id": credential_id, } data_raw = self.raw_delete(URL_ADMIN_USER_CREDENTIAL.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakDeleteError) def logout(self, user_id): """ @@ -676,7 +682,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_post(URL_ADMIN_USER_LOGOUT.format(**params_path), data="") - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def consents_user(self, user_id): """ @@ -719,6 +725,7 @@ class KeycloakAdmin: data_raw = self.raw_post( URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), data=json.dumps(payload) ) + return raise_error_from_response(data_raw, KeycloakPostError) def delete_user_social_login(self, user_id, provider_id): @@ -730,7 +737,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} data_raw = self.raw_delete(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def send_update_account( self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None @@ -754,7 +761,7 @@ class KeycloakAdmin: data=json.dumps(payload), **params_query ) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPutError) def send_verify_email(self, user_id, client_id=None, redirect_uri=None): """ @@ -772,7 +779,7 @@ class KeycloakAdmin: data_raw = self.raw_put( URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), data={}, **params_query ) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPutError) def get_sessions(self, user_id): """ @@ -931,7 +938,7 @@ class KeycloakAdmin: ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def update_group(self, group_id, payload): @@ -949,7 +956,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_put(URL_ADMIN_GROUP.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def group_set_permissions(self, group_id, enabled=True): """ @@ -965,7 +972,7 @@ class KeycloakAdmin: URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), data=json.dumps({"enabled": enabled}), ) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPutError) def group_user_add(self, user_id, group_id): """ @@ -978,7 +985,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_put(URL_ADMIN_USER_GROUP.format(**params_path), data=None) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def group_user_remove(self, user_id, group_id): """ @@ -991,7 +998,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_delete(URL_ADMIN_USER_GROUP.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def delete_group(self, group_id): """ @@ -1003,7 +1010,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_delete(URL_ADMIN_GROUP.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_clients(self): """ @@ -1063,7 +1070,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_SETTINGS.format(**params_path)) - return data_raw + return raise_error_from_response(data_raw, KeycloakGetError) def create_client_authz_resource(self, client_id, payload, skip_exists=False): """ @@ -1083,7 +1090,7 @@ class KeycloakAdmin: URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def get_client_authz_resources(self, client_id): @@ -1129,7 +1136,7 @@ class KeycloakAdmin: data=json.dumps(payload), ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def create_client_authz_resource_based_permission(self, client_id, payload, skip_exists=False): @@ -1163,7 +1170,7 @@ class KeycloakAdmin: data=json.dumps(payload), ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def get_client_authz_scopes(self, client_id): @@ -1177,7 +1184,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path)) - return data_raw + return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_permissions(self, client_id): """ @@ -1190,7 +1197,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS.format(**params_path)) - return data_raw + return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_policies(self, client_id): """ @@ -1203,7 +1210,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path)) - return data_raw + return raise_error_from_response(data_raw, KeycloakGetError) def get_client_service_account_user(self, client_id): """ @@ -1232,7 +1239,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(payload)) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def update_client(self, client_id, payload): @@ -1246,7 +1253,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_put(URL_ADMIN_CLIENT.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_client(self, client_id): """ @@ -1261,7 +1268,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_delete(URL_ADMIN_CLIENT.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_client_installation_provider(self, client_id, provider_id): """ @@ -1374,7 +1381,7 @@ class KeycloakAdmin: URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): @@ -1393,7 +1400,7 @@ class KeycloakAdmin: URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE.format(**params_path), data=json.dumps(payload), ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def delete_client_role(self, client_role_id, role_name): """ @@ -1407,7 +1414,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} data_raw = self.raw_delete(URL_ADMIN_CLIENT_ROLE.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def assign_client_role(self, user_id, client_id, roles): """ @@ -1424,7 +1431,7 @@ class KeycloakAdmin: data_raw = self.raw_post( URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def get_client_role_members(self, client_id, role_name, **query): """ @@ -1451,7 +1458,7 @@ class KeycloakAdmin: URL_ADMIN_REALM_ROLES.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def get_realm_role(self, role_name): @@ -1479,7 +1486,7 @@ class KeycloakAdmin: data_raw = self.raw_put( URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_realm_role(self, role_name): """ @@ -1490,7 +1497,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_delete(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_composite_realm_roles_to_role(self, role_name, roles): """ @@ -1507,7 +1514,7 @@ class KeycloakAdmin: URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), data=json.dumps(payload), ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def remove_composite_realm_roles_to_role(self, role_name, roles): """ @@ -1524,7 +1531,7 @@ class KeycloakAdmin: URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), data=json.dumps(payload), ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_composite_realm_roles_of_role(self, role_name): """ @@ -1552,7 +1559,7 @@ class KeycloakAdmin: data_raw = self.raw_post( URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def delete_realm_roles_of_user(self, user_id, roles): """ @@ -1568,7 +1575,7 @@ class KeycloakAdmin: data_raw = self.raw_delete( URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_realm_roles_of_user(self, user_id): """ @@ -1616,7 +1623,7 @@ class KeycloakAdmin: data_raw = self.raw_post( URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def delete_group_realm_roles(self, group_id, roles): """ @@ -1632,7 +1639,7 @@ class KeycloakAdmin: data_raw = self.raw_delete( URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_group_realm_roles(self, group_id): """ @@ -1660,7 +1667,7 @@ class KeycloakAdmin: data_raw = self.raw_post( URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def get_group_client_roles(self, group_id, client_id): """ @@ -1690,7 +1697,7 @@ class KeycloakAdmin: data_raw = self.raw_delete( URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_client_roles_of_user(self, user_id, client_id): """ @@ -1745,7 +1752,7 @@ class KeycloakAdmin: data_raw = self.raw_delete( URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_authentication_flows(self): """ @@ -1789,7 +1796,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path), data=json.dumps(payload)) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def copy_authentication_flow(self, payload, flow_alias): @@ -1805,7 +1812,7 @@ class KeycloakAdmin: data_raw = self.raw_post( URL_ADMIN_FLOWS_COPY.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def delete_authentication_flow(self, flow_id): """ @@ -1819,7 +1826,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": flow_id} data_raw = self.raw_delete(URL_ADMIN_FLOW.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_authentication_flow_executions(self, flow_alias): """ @@ -1848,7 +1855,7 @@ class KeycloakAdmin: data_raw = self.raw_put( URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[202, 204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[202, 204]) def get_authentication_flow_execution(self, execution_id): """ @@ -1880,7 +1887,7 @@ class KeycloakAdmin: data_raw = self.raw_post( URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def delete_authentication_flow_execution(self, execution_id): """ @@ -1894,7 +1901,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": execution_id} data_raw = self.raw_delete(URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False): """ @@ -1914,7 +1921,7 @@ class KeycloakAdmin: URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def get_authenticator_config(self, config_id): @@ -1943,7 +1950,7 @@ class KeycloakAdmin: data_raw = self.raw_put( URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_authenticator_config(self, config_id): """ @@ -1956,8 +1963,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": config_id} data_raw = self.raw_delete(URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path)) - - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def sync_users(self, storage_id, action): """ @@ -1974,7 +1980,7 @@ class KeycloakAdmin: data_raw = self.raw_post( URL_ADMIN_USER_STORAGE.format(**params_path), data=json.dumps(data), **params_query ) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def get_client_scopes(self): """ @@ -2017,7 +2023,7 @@ class KeycloakAdmin: URL_ADMIN_CLIENT_SCOPES.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def update_client_scope(self, client_scope_id, payload): @@ -2035,7 +2041,7 @@ class KeycloakAdmin: data_raw = self.raw_put( URL_ADMIN_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def add_mapper_to_client_scope(self, client_scope_id, payload): """ @@ -2053,7 +2059,7 @@ class KeycloakAdmin: URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def delete_mapper_from_client_scope(self, client_scope_id, protocol_mppaer_id): """ @@ -2072,8 +2078,7 @@ class KeycloakAdmin: } data_raw = self.raw_delete(URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path)) - - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + 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): """ @@ -2097,7 +2102,7 @@ class KeycloakAdmin: URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_default_default_client_scopes(self): """ @@ -2118,7 +2123,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": scope_id} data_raw = self.raw_delete(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_default_default_client_scope(self, scope_id): """ @@ -2132,7 +2137,7 @@ class KeycloakAdmin: data_raw = self.raw_put( URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_default_optional_client_scopes(self): """ @@ -2153,7 +2158,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": scope_id} data_raw = self.raw_delete(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_default_optional_client_scope(self, scope_id): """ @@ -2167,7 +2172,7 @@ class KeycloakAdmin: data_raw = self.raw_put( URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def add_mapper_to_client(self, client_id, payload): """ @@ -2185,7 +2190,7 @@ class KeycloakAdmin: URL_ADMIN_CLIENT_PROTOCOL_MAPPERS.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def update_client_mapper(self, client_id, mapper_id, payload): """ @@ -2206,7 +2211,7 @@ class KeycloakAdmin: URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def remove_client_mapper(self, client_id, client_mapper_id): """ @@ -2224,8 +2229,7 @@ class KeycloakAdmin: } data_raw = self.raw_delete(URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path)) - - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def generate_client_secrets(self, client_id): """ @@ -2239,7 +2243,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post(URL_ADMIN_CLIENT_SECRETS.format(**params_path), data=None) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def get_client_secrets(self, client_id): """ @@ -2285,7 +2289,7 @@ class KeycloakAdmin: data_raw = self.raw_post( URL_ADMIN_COMPONENTS.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def get_component(self, component_id): """ @@ -2316,7 +2320,7 @@ class KeycloakAdmin: data_raw = self.raw_put( URL_ADMIN_COMPONENT.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_component(self, component_id): """ @@ -2328,7 +2332,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_delete(URL_ADMIN_COMPONENT.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_keys(self): """ @@ -2367,7 +2371,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_put(URL_ADMIN_EVENTS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def raw_get(self, *args, **kwargs): """ From aff3051ffa33c2c50a2504fc42b5e176be955918 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 12:40:15 +0200 Subject: [PATCH 3/9] docs: fix docstrings --- keycloak/authorization/__init__.py | 4 +- keycloak/authorization/permission.py | 14 +++-- keycloak/authorization/policy.py | 7 ++- keycloak/connection.py | 87 +++++++++++++--------------- keycloak/keycloak_openid.py | 51 ++++++++-------- 5 files changed, 79 insertions(+), 84 deletions(-) diff --git a/keycloak/authorization/__init__.py b/keycloak/authorization/__init__.py index dad1078..789656d 100644 --- a/keycloak/authorization/__init__.py +++ b/keycloak/authorization/__init__.py @@ -38,7 +38,7 @@ class Authorization: """ def __init__(self): - self._policies = {} + self.policies = {} @property def policies(self): @@ -53,7 +53,7 @@ class Authorization: Load policies, roles and permissions (scope/resources). :param data: keycloak authorization data (dict) - :return: + :returns: None """ for pol in data["policies"]: if pol["type"] == "role": diff --git a/keycloak/authorization/permission.py b/keycloak/authorization/permission.py index 9988730..a200afe 100644 --- a/keycloak/authorization/permission.py +++ b/keycloak/authorization/permission.py @@ -26,15 +26,19 @@ class Permission: """ Consider this simple and very common permission: - A permission associates the object being protected with the policies that must be evaluated to determine whether access is granted. + A permission associates the object being protected with the policies that must be evaluated to + determine whether access is granted. X CAN DO Y ON RESOURCE Z - where … - X represents one or more users, roles, or groups, or a combination of them. You can + where + + - X represents one or more users, roles, or groups, or a combination of them. You can also use claims and context here. - Y represents an action to be performed, for example, write, view, and so on. - Z represents a protected resource, for example, "/accounts". + + - Y represents an action to be performed, for example, write, view, and so on. + + - Z represents a protected resource, for example, "/accounts". https://keycloak.gitbooks.io/documentation/authorization_services/topics/permission/overview.html diff --git a/keycloak/authorization/policy.py b/keycloak/authorization/policy.py index 4fbe913..4014b7a 100644 --- a/keycloak/authorization/policy.py +++ b/keycloak/authorization/policy.py @@ -29,9 +29,10 @@ class Policy: 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). - Policies are strongly related to the different access control mechanisms (ACMs) that you can use to - protect your resources. With policies, you can implement strategies for attribute-based access control - (ABAC), role-based access control (RBAC), context-based access control, or any combination of these. + Policies are strongly related to the different access control mechanisms (ACMs) that you can + use to protect your resources. With policies, you can implement strategies for attribute-based + access control (ABAC), role-based access control (RBAC), context-based access control, or any + combination of these. https://keycloak.gitbooks.io/documentation/authorization_services/topics/policy/overview.html diff --git a/keycloak/connection.py b/keycloak/connection.py index 8ef45b1..0757377 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -33,13 +33,14 @@ from .exceptions import KeycloakConnectionError class ConnectionManager(object): - """Represents a simple server connection. - Args: - base_url (str): The server URL. - headers (dict): The header parameters of the requests to the server. - timeout (int): Timeout to use for requests to the server. - verify (bool): Verify server SSL. - proxies (dict): The proxies servers requests is sent by. + """ + Represents a simple server connection. + + :param base_url: (str) The server URL. + :param headers: (dict) The header parameters of the requests to the server. + :param timeout: (int) Timeout to use for requests to the server. + :param verify: (bool) Verify server SSL. + :param proxies: (dict) The proxies servers requests is sent by. """ def __init__(self, base_url, headers={}, timeout=60, verify=True, proxies=None): @@ -108,11 +109,11 @@ class ConnectionManager(object): self._headers = value def param_headers(self, key): - """Return a specific header parameter. - :arg - key (str): Header parameters key. - :return: - If the header parameters exist, return its value. + """ + Return a specific header parameter. + + :param key: (str) Header parameters key. + :returns: If the header parameters exist, return its value. """ return self.headers.get(key) @@ -122,36 +123,33 @@ class ConnectionManager(object): def exist_param_headers(self, key): """Check if the parameter exists in the header. - :arg - key (str): Header parameters key. - :return: - If the header parameters exist, return True. + + :param key: (str) Header parameters key. + :returns: If the header parameters exist, return True. """ return self.param_headers(key) is not None def add_param_headers(self, key, value): """Add a single parameter inside the header. - :arg - key (str): Header parameters key. - value (str): Value to be added. + + :param key: (str) Header parameters key. + :param value: (str) Value to be added. """ self.headers[key] = value def del_param_headers(self, key): """Remove a specific parameter. - :arg - key (str): Key of the header parameters. + + :param key: (str) Key of the header parameters. """ self.headers.pop(key, None) def raw_get(self, path, **kwargs): """Submit get request to the path. - :arg - path (str): Path for request. - :return - Response the request. - :exception - HttpError: Can't connect to server. + + :param path: (str) Path for request. + :returns: Response the request. + :raises: HttpError Can't connect to server. """ try: @@ -167,13 +165,11 @@ class ConnectionManager(object): def raw_post(self, path, data, **kwargs): """Submit post request to the path. - :arg - path (str): Path for request. - data (dict): Payload for request. - :return - Response the request. - :exception - HttpError: Can't connect to server. + + :param path: (str) Path for request. + :param data: (dict) Payload for request. + :returns: Response the request. + :raises: HttpError Can't connect to server. """ try: return self._s.post( @@ -189,13 +185,11 @@ class ConnectionManager(object): def raw_put(self, path, data, **kwargs): """Submit put request to the path. - :arg - path (str): Path for request. - data (dict): Payload for request. - :return - Response the request. - :exception - HttpError: Can't connect to server. + + :param path: (str) Path for request. + :param data: (dict) Payload for request. + :returns: Response the request. + :raises: HttpError Can't connect to server. """ try: return self._s.put( @@ -212,13 +206,10 @@ class ConnectionManager(object): def raw_delete(self, path, data={}, **kwargs): """Submit delete request to the path. - :arg - path (str): Path for request. - data (dict): Payload for request. - :return - Response the request. - :exception - HttpError: Can't connect to server. + :param path: (str) Path for request. + :param data: (dict) Payload for request. + :returns: Response the request. + :raises: HttpError Can't connect to server. """ try: return self._s.delete( diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index d9c068f..4205b0b 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -49,6 +49,18 @@ from .urls_patterns import ( class KeycloakOpenID: + """ + Keycloak OpenID client. + + :param server_url: Keycloak server url + :param client_id: client id + :param realm_name: realm name + :param client_secret_key: client secret key + :param verify: True if want check connection SSL + :param custom_headers: dict of custom header to pass to each HTML request + :param proxies: dict of proxies to sent the request by. + """ + def __init__( self, server_url, @@ -59,28 +71,15 @@ class KeycloakOpenID: custom_headers=None, proxies=None, ): - """ - - :param server_url: Keycloak server url - :param client_id: client id - :param realm_name: realm name - :param client_secret_key: client secret key - :param verify: True if want check connection SSL - :param custom_headers: dict of custom header to pass to each HTML request - :param proxies: dict of proxies to sent the request by. - """ - self._client_id = client_id - self._client_secret_key = client_secret_key - self._realm_name = realm_name - headers = dict() - if custom_headers is not None: - # merge custom headers to main headers - headers.update(custom_headers) - self._connection = ConnectionManager( + self.client_id = client_id + self.client_secret_key = client_secret_key + self.realm_name = realm_name + headers = custom_headers if custom_headers is not None else dict() + self.connection = ConnectionManager( base_url=server_url, headers=headers, timeout=60, verify=verify, proxies=proxies ) - self._authorization = Authorization() + self.authorization = Authorization() @property def client_id(self): @@ -206,8 +205,8 @@ class KeycloakOpenID: :param password: :param grant_type: :param code: - :param redirect_uri - :param totp + :param redirect_uri: + :param totp: :return: """ params_path = {"realm-name": self.realm_name} @@ -312,9 +311,9 @@ class KeycloakOpenID: """ 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 authorization - policies associated with the resources being requested. With an RPT, client applications can - gain access to protected resources at the resource server. + (or permissions) for a user as a result of the evaluation of the permissions and + authorization policies associated with the resources being requested. With an RPT, + client applications can gain access to protected resources at the resource server. :return: """ @@ -329,8 +328,8 @@ class KeycloakOpenID: def introspect(self, token, rpt=None, token_type_hint=None): """ - The introspection endpoint is used to retrieve the active state of a token. It is can only be - invoked by confidential clients. + The introspection endpoint is used to retrieve the active state of a token. + It is can only be invoked by confidential clients. https://tools.ietf.org/html/rfc7662 From b911d94db9b44257cb8535457062da33525f7f11 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 12:41:38 +0200 Subject: [PATCH 4/9] feat: fixed admin client to pass the tests --- keycloak/_version.py | 23 +++++ keycloak/keycloak_admin.py | 166 ++++++++++++++++++++++++++++--------- keycloak/urls_patterns.py | 8 +- 3 files changed, 155 insertions(+), 42 deletions(-) diff --git a/keycloak/_version.py b/keycloak/_version.py index 6c8e6b9..f3403b2 100644 --- a/keycloak/_version.py +++ b/keycloak/_version.py @@ -1 +1,24 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (C) 2017 Marcos Pereira +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# 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. + __version__ = "0.0.0" diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 3e30722..8d6463b 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -61,6 +61,7 @@ from .urls_patterns import ( URL_ADMIN_CLIENT_SCOPES_MAPPERS, URL_ADMIN_CLIENT_SECRETS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, + URL_ADMIN_CLIENT_ROLE_GROUPS, URL_ADMIN_CLIENTS, URL_ADMIN_COMPONENT, URL_ADMIN_COMPONENTS, @@ -68,7 +69,6 @@ from .urls_patterns import ( URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES, URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE, URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES, - URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_EVENTS, URL_ADMIN_FLOW, URL_ADMIN_FLOWS, @@ -123,6 +123,23 @@ from .urls_patterns import ( class KeycloakAdmin: + """ + Keycloak Admin client. + + :param server_url: Keycloak server url + :param username: admin username + :param password: admin password + :param totp: Time based OTP + :param realm_name: realm name + :param client_id: client id + :param verify: True if want check connection SSL + :param client_secret_key: client secret key + (optional, required only for access type confidential) + :param custom_headers: dict of custom header to pass to each HTML request + :param user_realm_name: The realm name of the user, if different from realm_name + :param auto_refresh_token: list of methods that allows automatic token refresh. + Ex: ['get', 'put', 'post', 'delete'] + """ PAGE_SIZE = 100 @@ -154,20 +171,6 @@ class KeycloakAdmin: user_realm_name=None, auto_refresh_token=None, ): - """ - - :param server_url: Keycloak server url - :param username: admin username - :param password: admin password - :param totp: Time based OTP - :param realm_name: realm name - :param client_id: client id - :param verify: True if want check connection SSL - :param client_secret_key: client secret key (optional, required only for access type confidential) - :param custom_headers: dict of custom header to pass to each HTML request - :param user_realm_name: The realm name of the user, if different from realm_name - :param auto_refresh_token: list of methods that allows automatic token refresh. ex: ['get', 'put', 'post', 'delete'] - """ self.server_url = server_url self.username = username self.password = password @@ -378,6 +381,20 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_REALMS) return raise_error_from_response(data_raw, KeycloakGetError) + def get_realm(self, realm_name): + """ + Get a specific realm. + + RealmRepresentation: + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation + + :param realm_name: Realm name (not the realm id) + :return: RealmRepresentation + """ + params_path = {"realm-name": realm_name} + data_raw = self.raw_get(URL_ADMIN_REALM.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) + def create_realm(self, payload, skip_exists=False): """ Create a realm @@ -495,7 +512,7 @@ class KeycloakAdmin: data_raw = self.raw_delete(URL_ADMIN_IDP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) - def create_user(self, payload, exist_ok=True): + def create_user(self, payload, exist_ok=False): """ Create a new user. Username must be unique @@ -530,6 +547,33 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_USERS_COUNT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def user_logout(self, user_id): + """ + Logs out user. + + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_logout + + :param user_id: User id + :return: + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_post(URL_ADMIN_USER_LOGOUT.format(**params_path), data="") + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) + + def user_consents(self, user_id): + """ + Get consents granted by the user + + UserConsentRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userconsentrepresentation + + :param user_id: User id + :return: List of UserConsentRepresentations + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_get(URL_ADMIN_USER_CONSENTS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def get_user_id(self, username): """ Get internal keycloak user id from username @@ -923,7 +967,7 @@ class KeycloakAdmin: GroupRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation - :return: Http response + :return: Group ID for newly created group, otherwise None """ if parent is None: @@ -937,9 +981,14 @@ class KeycloakAdmin: URL_ADMIN_GROUP_CHILD.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response( + raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) + try: + _last_slash_idx = data_raw.headers["Location"].rindex("/") + return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 + except KeyError: + return def update_group(self, group_id, payload): """ @@ -1229,18 +1278,27 @@ class KeycloakAdmin: """ Create a client - ClientRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + ClientRepresentation: + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :param skip_exists: If true then do not raise an error if client already exists :param payload: ClientRepresentation - :return: Keycloak server response (UserRepresentation) + :return: Client ID """ + if skip_exists: + client_id = self.get_client_id(client_name=payload["name"]) + + if client_id is not None: + return client_id + params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response( + raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) + _last_slash_idx = data_raw.headers["Location"].rindex("/") + return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def update_client(self, client_id, payload): """ @@ -1368,21 +1426,46 @@ class KeycloakAdmin: Create a client role RoleRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :param client_role_id: id of client (not client-id) :param payload: RoleRepresentation :param skip_exists: If true then do not raise an error if client role already exists - :return: Keycloak server response (RoleRepresentation) + :return: Client role name """ + if skip_exists: + res = self.get_client_role(client_id=client_role_id, role_name=payload["name"]) + if res: + return res["name"] + params_path = {"realm-name": self.realm_name, "id": client_role_id} data_raw = self.raw_post( URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response( + raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) + _last_slash_idx = data_raw.headers["Location"].rindex("/") + return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 + + def update_client_role(self, client_role_id, role_name, payload): + """ + Update a client role + + RoleRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation + + :param client_role_id: id of client (not client-id) + :param role_name: role's name (not id!) + :param payload: RoleRepresentation + """ + params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} + data_raw = self.raw_put( + URL_ADMIN_CLIENT_ROLE.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): """ @@ -1444,22 +1527,41 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_MEMBERS.format(**params_path), query) + def get_client_role_groups(self, client_id, role_name, **query): + """ + 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 + (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} + return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_GROUPS.format(**params_path), query) + def create_realm_role(self, payload, skip_exists=False): """ 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 Keycloak server response + :return: Realm role name """ + if skip_exists: + role = self.get_realm_role(role_name=payload["name"]) + if role is not None: + return role["name"] + params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( URL_ADMIN_REALM_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response( + raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) + _last_slash_idx = data_raw.headers["Location"].rindex("/") + return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def get_realm_role(self, role_name): """ @@ -2506,18 +2608,6 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_CLIENT_ALL_SESSIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def delete_user_realm_role(self, user_id, payload): - """ - Delete realm-level role mappings - DELETE admin/realms/{realm-name}/users/{id}/role-mappings/realm - - """ - params_path = {"realm-name": self.realm_name, "id": str(user_id)} - data_raw = self.raw_delete( - URL_ADMIN_DELETE_USER_ROLE.format(**params_path), data=json.dumps(payload) - ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def get_client_sessions_stats(self): """ Get current session count for all clients with active sessions diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 43699eb..34d8514 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -65,7 +65,6 @@ URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE = ( ) URL_ADMIN_USER_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}" URL_ADMIN_USER_GROUPS = "admin/realms/{realm-name}/users/{id}/groups" -URL_ADMIN_USER_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password" URL_ADMIN_USER_CREDENTIALS = "admin/realms/{realm-name}/users/{id}/credentials" URL_ADMIN_USER_CREDENTIAL = "admin/realms/{realm-name}/users/{id}/credentials/{credential_id}" URL_ADMIN_USER_LOGOUT = "admin/realms/{realm-name}/users/{id}/logout" @@ -87,12 +86,15 @@ URL_ADMIN_CLIENT_ROLES = URL_ADMIN_CLIENT + "/roles" URL_ADMIN_CLIENT_ROLE = URL_ADMIN_CLIENT + "/roles/{role-name}" URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE = URL_ADMIN_CLIENT_ROLE + "/composites" URL_ADMIN_CLIENT_ROLE_MEMBERS = URL_ADMIN_CLIENT + "/roles/{role-name}/users" +URL_ADMIN_CLIENT_ROLE_GROUPS = URL_ADMIN_CLIENT + "/roles/{role-name}/groups" URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource?max=-1" URL_ADMIN_CLIENT_AUTHZ_SCOPES = URL_ADMIN_CLIENT + "/authz/resource-server/scope?max=-1" URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS = URL_ADMIN_CLIENT + "/authz/resource-server/permission?max=-1" -URL_ADMIN_CLIENT_AUTHZ_POLICIES = URL_ADMIN_CLIENT + "/authz/resource-server/policy?max=-1" +URL_ADMIN_CLIENT_AUTHZ_POLICIES = ( + URL_ADMIN_CLIENT + "/authz/resource-server/policy?max=-1&permission=false" +) URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY = ( URL_ADMIN_CLIENT + "/authz/resource-server/policy/role?max=-1" ) @@ -155,6 +157,4 @@ URL_ADMIN_USER_FEDERATED_IDENTITY = ( ) URL_ADMIN_EVENTS = "admin/realms/{realm-name}/events" - -URL_ADMIN_DELETE_USER_ROLE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" URL_ADMIN_CLIENT_SESSION_STATS = "admin/realms/{realm-name}/client-session-stats" From 6cce29f26b2f0735937f3f6b247dcb6538a3bc05 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 12:51:07 +0200 Subject: [PATCH 5/9] fix: full tox fix ready --- keycloak/keycloak_admin.py | 826 ++++++++++++++++++------------------- keycloak/urls_patterns.py | 5 +- 2 files changed, 412 insertions(+), 419 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 8d6463b..7b92d80 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -28,6 +28,7 @@ import json from builtins import isinstance from typing import Iterable +from . import urls_patterns from .connection import ConnectionManager from .exceptions import ( KeycloakDeleteError, @@ -37,89 +38,6 @@ from .exceptions import ( raise_error_from_response, ) from .keycloak_openid import KeycloakOpenID -from .urls_patterns import ( - URL_ADMIN_AUTHENTICATOR_CONFIG, - URL_ADMIN_CLIENT, - URL_ADMIN_CLIENT_ALL_SESSIONS, - URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS, - URL_ADMIN_CLIENT_AUTHZ_POLICIES, - URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION, - URL_ADMIN_CLIENT_AUTHZ_RESOURCES, - URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY, - URL_ADMIN_CLIENT_AUTHZ_SCOPES, - URL_ADMIN_CLIENT_AUTHZ_SETTINGS, - URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, - URL_ADMIN_CLIENT_PROTOCOL_MAPPER, - URL_ADMIN_CLIENT_PROTOCOL_MAPPERS, - URL_ADMIN_CLIENT_ROLE, - URL_ADMIN_CLIENT_ROLE_MEMBERS, - URL_ADMIN_CLIENT_ROLES, - URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, - URL_ADMIN_CLIENT_SCOPE, - URL_ADMIN_CLIENT_SCOPES, - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, - URL_ADMIN_CLIENT_SCOPES_MAPPERS, - URL_ADMIN_CLIENT_SECRETS, - URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, - URL_ADMIN_CLIENT_ROLE_GROUPS, - URL_ADMIN_CLIENTS, - URL_ADMIN_COMPONENT, - URL_ADMIN_COMPONENTS, - URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE, - URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES, - URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE, - URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES, - URL_ADMIN_EVENTS, - URL_ADMIN_FLOW, - URL_ADMIN_FLOWS, - URL_ADMIN_FLOWS_ALIAS, - URL_ADMIN_FLOWS_COPY, - URL_ADMIN_FLOWS_EXECUTION, - URL_ADMIN_FLOWS_EXECUTIONS, - URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION, - URL_ADMIN_FLOWS_EXECUTIONS_FLOW, - URL_ADMIN_GET_SESSIONS, - URL_ADMIN_GROUP, - URL_ADMIN_GROUP_CHILD, - URL_ADMIN_GROUP_MEMBERS, - URL_ADMIN_GROUP_PERMISSIONS, - URL_ADMIN_GROUPS, - URL_ADMIN_GROUPS_CLIENT_ROLES, - URL_ADMIN_GROUPS_REALM_ROLES, - URL_ADMIN_IDP, - URL_ADMIN_IDP_MAPPERS, - URL_ADMIN_IDPS, - URL_ADMIN_KEYS, - URL_ADMIN_REALM, - URL_ADMIN_REALM_EXPORT, - URL_ADMIN_REALM_ROLES, - URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, - URL_ADMIN_REALM_ROLES_MEMBERS, - URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, - URL_ADMIN_REALMS, - URL_ADMIN_RESET_PASSWORD, - URL_ADMIN_SEND_UPDATE_ACCOUNT, - URL_ADMIN_SEND_VERIFY_EMAIL, - URL_ADMIN_SERVER_INFO, - URL_ADMIN_USER, - URL_ADMIN_USER_CLIENT_ROLES, - URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, - URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, - URL_ADMIN_USER_CONSENTS, - URL_ADMIN_USER_CREDENTIAL, - URL_ADMIN_USER_CREDENTIALS, - URL_ADMIN_USER_FEDERATED_IDENTITIES, - URL_ADMIN_USER_FEDERATED_IDENTITY, - URL_ADMIN_USER_GROUP, - URL_ADMIN_USER_GROUPS, - URL_ADMIN_USER_LOGOUT, - URL_ADMIN_USER_REALM_ROLES, - URL_ADMIN_USER_REALM_ROLES_AVAILABLE, - URL_ADMIN_USER_REALM_ROLES_COMPOSITE, - URL_ADMIN_USER_STORAGE, - URL_ADMIN_USERS, - URL_ADMIN_USERS_COUNT, -) class KeycloakAdmin: @@ -342,14 +260,14 @@ class KeycloakAdmin: Import a new realm from a RealmRepresentation. Realm name must be unique. RealmRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmrepresentation :param payload: RealmRepresentation :return: RealmRepresentation """ - data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) + 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): @@ -357,7 +275,7 @@ class KeycloakAdmin: Export the realm configurations in the json format RealmRepresentation - https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_partialexport + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_partialexport :param export-clients: Skip if not want to export realm clients :param export-groups-and-roles: Skip if not want to export realm groups and roles @@ -369,7 +287,9 @@ class KeycloakAdmin: "export-clients": export_clients, "export-groups-and-roles": export_groups_and_role, } - data_raw = self.raw_post(URL_ADMIN_REALM_EXPORT.format(**params_path), data="") + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_REALM_EXPORT.format(**params_path), data="" + ) return raise_error_from_response(data_raw, KeycloakPostError) def get_realms(self): @@ -378,7 +298,7 @@ class KeycloakAdmin: :return: realms list """ - data_raw = self.raw_get(URL_ADMIN_REALMS) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_REALMS) return raise_error_from_response(data_raw, KeycloakGetError) def get_realm(self, realm_name): @@ -392,7 +312,7 @@ class KeycloakAdmin: :return: RealmRepresentation """ params_path = {"realm-name": realm_name} - data_raw = self.raw_get(URL_ADMIN_REALM.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_REALM.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) def create_realm(self, payload, skip_exists=False): @@ -400,14 +320,14 @@ class KeycloakAdmin: Create a realm RealmRepresentation: - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmrepresentation :param payload: RealmRepresentation :param skip_exists: Skip if Realm already exist. :return: Keycloak server response (RealmRepresentation) """ - data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) + 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 ) @@ -418,7 +338,7 @@ class KeycloakAdmin: role, or client information in the payload. RealmRepresentation: - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmrepresentation :param realm_name: Realm name (not the realm id) :param payload: RealmRepresentation @@ -426,7 +346,9 @@ class KeycloakAdmin: """ params_path = {"realm-name": realm_name} - data_raw = self.raw_put(URL_ADMIN_REALM.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_REALM.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_realm(self, realm_name): @@ -438,7 +360,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": realm_name} - data_raw = self.raw_delete(URL_ADMIN_REALM.format(**params_path)) + 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): @@ -446,14 +368,14 @@ class KeycloakAdmin: Return a list of users, filtered according to query parameters UserRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation :param query: Query parameters (optional) :return: users list """ query = query or {} params_path = {"realm-name": self.realm_name} - url = URL_ADMIN_USERS.format(**params_path) + url = urls_patterns.URL_ADMIN_USERS.format(**params_path) if "first" in query or "max" in query: return self.__fetch_paginated(url, query) @@ -465,12 +387,14 @@ class KeycloakAdmin: Create an ID Provider, IdentityProviderRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityproviderrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_identityproviderrepresentation :param: payload: IdentityProviderRepresentation """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_IDPS.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_IDPS.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def add_mapper_to_idp(self, idp_alias, payload): @@ -478,14 +402,14 @@ class KeycloakAdmin: Create an ID Provider, IdentityProviderRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityprovidermapperrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_identityprovidermapperrepresentation :param: idp_alias: alias for Idp to add mapper in :param: payload: IdentityProviderMapperRepresentation """ params_path = {"realm-name": self.realm_name, "idp-alias": idp_alias} data_raw = self.raw_post( - URL_ADMIN_IDP_MAPPERS.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_IDP_MAPPERS.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -494,12 +418,12 @@ class KeycloakAdmin: Returns a list of ID Providers, IdentityProviderRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityproviderrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_identityproviderrepresentation :return: array IdentityProviderRepresentation """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_IDPS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_IDPS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def delete_idp(self, idp_alias): @@ -509,7 +433,7 @@ class KeycloakAdmin: :param: idp_alias: idp alias name """ params_path = {"realm-name": self.realm_name, "alias": idp_alias} - data_raw = self.raw_delete(URL_ADMIN_IDP.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_IDP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def create_user(self, payload, exist_ok=False): @@ -517,10 +441,11 @@ class KeycloakAdmin: Create a new user. Username must be unique UserRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation :param payload: UserRepresentation - :param exist_ok: If False, raise KeycloakGetError if username already exists. Otherwise, return existing user ID. + :param exist_ok: If False, raise KeycloakGetError if username already exists. + Otherwise, return existing user ID. :return: UserRepresentation """ @@ -532,10 +457,12 @@ class KeycloakAdmin: if exists is not None: return str(exists) - data_raw = self.raw_post(URL_ADMIN_USERS.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_USERS.format(**params_path), data=json.dumps(payload) + ) raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) _last_slash_idx = data_raw.headers["Location"].rindex("/") - return data_raw.headers["Location"][_last_slash_idx + 1 :] + return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def users_count(self): """ @@ -544,34 +471,7 @@ class KeycloakAdmin: :return: counter """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_USERS_COUNT.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError) - - def user_logout(self, user_id): - """ - Logs out user. - - https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_logout - - :param user_id: User id - :return: - """ - params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_post(URL_ADMIN_USER_LOGOUT.format(**params_path), data="") - return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) - - def user_consents(self, user_id): - """ - Get consents granted by the user - - UserConsentRepresentation - https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userconsentrepresentation - - :param user_id: User id - :return: List of UserConsentRepresentations - """ - params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_CONSENTS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_USERS_COUNT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_user_id(self, username): @@ -580,7 +480,7 @@ class KeycloakAdmin: This is required for further actions against this user. UserRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation :param username: id in UserRepresentation @@ -597,12 +497,12 @@ class KeycloakAdmin: :param user_id: User id UserRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation :return: UserRepresentation """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_user_groups(self, user_id): @@ -614,7 +514,7 @@ class KeycloakAdmin: :return: user groups list """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_GROUPS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_GROUPS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def update_user(self, user_id, payload): @@ -627,7 +527,9 @@ class KeycloakAdmin: :return: Http response """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_put(URL_ADMIN_USER.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_USER.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_user(self, user_id): @@ -639,7 +541,7 @@ class KeycloakAdmin: :return: Http response """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_delete(URL_ADMIN_USER.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_USER.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def set_user_password(self, user_id, password, temporary=True): @@ -647,8 +549,8 @@ class KeycloakAdmin: 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/8.0/rest-api/#_users_resource - https://www.keycloak.org/docs-api/8.0/rest-api/#_credentialrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_users_resource + https://www.keycloak.org/docs-api/18.0/rest-api/#_credentialrepresentation :param user_id: User id :param password: New password @@ -659,7 +561,7 @@ class KeycloakAdmin: payload = {"type": "password", "temporary": temporary, "value": password} params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_put( - URL_ADMIN_RESET_PASSWORD.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_RESET_PASSWORD.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) @@ -668,32 +570,13 @@ class KeycloakAdmin: Returns a list of credential belonging to the user. CredentialRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_credentialrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_credentialrepresentation :param: user_id: user id :return: Keycloak server response (CredentialRepresentation) """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_CREDENTIALS.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError) - - def get_credential(self, user_id, credential_id): - """ - Get credential of the user. - - CredentialRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_credentialrepresentation - - :param: user_id: user id - :param: credential_id: credential id - :return: Keycloak server response (ClientRepresentation) - """ - params_path = { - "realm-name": self.realm_name, - "id": user_id, - "credential_id": credential_id, - } - data_raw = self.raw_get(URL_ADMIN_USER_CREDENTIAL.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_CREDENTIALS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def delete_credential(self, user_id, credential_id): @@ -701,7 +584,7 @@ class KeycloakAdmin: Delete credential of the user. CredentialRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_credentialrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_credentialrepresentation :param: user_id: user id :param: credential_id: credential id @@ -712,42 +595,49 @@ class KeycloakAdmin: "id": user_id, "credential_id": credential_id, } - data_raw = self.raw_delete(URL_ADMIN_USER_CREDENTIAL.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_USER_CREDENTIAL.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError) - def logout(self, user_id): + def user_logout(self, user_id): """ Logs out user. - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_logout + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_logout :param user_id: User id :return: """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_post(URL_ADMIN_USER_LOGOUT.format(**params_path), data="") + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_USER_LOGOUT.format(**params_path), data="" + ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) - def consents_user(self, user_id): + def user_consents(self, user_id): """ Get consents granted by the user - :param user_id: User id + UserConsentRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userconsentrepresentation - :return: consents + :param user_id: User id + :return: List of UserConsentRepresentations """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_CONSENTS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_CONSENTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_user_social_logins(self, user_id): """ - Returns a list of federated identities/social logins of which the user has been associated with + Returns a list of federated identities/social logins of which the user has been associated + with :param user_id: User id :return: federated identities list """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_FEDERATED_IDENTITIES.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_USER_FEDERATED_IDENTITIES.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def add_user_social_login(self, user_id, provider_id, provider_userid, provider_username): @@ -767,9 +657,10 @@ class KeycloakAdmin: } params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} data_raw = self.raw_post( - URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), + data=json.dumps(payload), ) - return raise_error_from_response(data_raw, KeycloakPostError) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201, 204]) def delete_user_social_login(self, user_id, provider_id): @@ -780,7 +671,9 @@ class KeycloakAdmin: :return: """ params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} - data_raw = self.raw_delete(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path)) + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def send_update_account( @@ -801,7 +694,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id} params_query = {"client_id": client_id, "lifespan": lifespan, "redirect_uri": redirect_uri} data_raw = self.raw_put( - URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), + urls_patterns.URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), data=json.dumps(payload), **params_query ) @@ -821,7 +714,9 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id} params_query = {"client_id": client_id, "redirect_uri": redirect_uri} data_raw = self.raw_put( - URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), data={}, **params_query + urls_patterns.URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), + data={}, + **params_query ) return raise_error_from_response(data_raw, KeycloakPutError) @@ -832,12 +727,12 @@ class KeycloakAdmin: :param user_id: id of user UserSessionRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_usersessionrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_usersessionrepresentation :return: UserSessionRepresentation """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_GET_SESSIONS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_GET_SESSIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_server_info(self): @@ -845,11 +740,11 @@ class KeycloakAdmin: Get themes, social providers, auth providers, and event listeners available on this server ServerInfoRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_serverinforepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_serverinforepresentation :return: ServerInfoRepresentation """ - data_raw = self.raw_get(URL_ADMIN_SERVER_INFO) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_SERVER_INFO) return raise_error_from_response(data_raw, KeycloakGetError) def get_groups(self, query=None): @@ -857,13 +752,13 @@ class KeycloakAdmin: Returns a list of groups belonging to the realm GroupRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation :return: array GroupRepresentation """ query = query or {} params_path = {"realm-name": self.realm_name} - url = URL_ADMIN_GROUPS.format(**params_path) + url = urls_patterns.URL_ADMIN_GROUPS.format(**params_path) if "first" in query or "max" in query: return self.__fetch_paginated(url, query) @@ -875,13 +770,13 @@ class KeycloakAdmin: Get group by id. Returns full group details GroupRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation :param group_id: The group id :return: Keycloak server response (GroupRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_get(URL_ADMIN_GROUP.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_subgroups(self, group, path): @@ -889,7 +784,7 @@ class KeycloakAdmin: Utility function to iterate through nested group structures GroupRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation :param name: group (GroupRepresentation) :param path: group path (string) @@ -908,19 +803,21 @@ class KeycloakAdmin: # went through the tree without hits return None - def get_group_members(self, group_id, **query): + def get_group_members(self, group_id, query=None): """ Get members by group id. Returns group members GroupRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/#_userrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_userrepresentation :param group_id: The group id - :param query: Additional query parameters (see https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getmembers) + :param query: Additional query parameters + (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getmembers) :return: Keycloak server response (UserRepresentation) """ + query = query or {} params_path = {"realm-name": self.realm_name, "id": group_id} - url = URL_ADMIN_GROUP_MEMBERS.format(**params_path) + url = urls_patterns.URL_ADMIN_GROUP_MEMBERS.format(**params_path) if "first" in query or "max" in query: return self.__fetch_paginated(url, query) @@ -934,7 +831,7 @@ class KeycloakAdmin: Subgroups are traversed, the first to match path (or name with path) is returned. GroupRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation :param path: group path :param search_in_subgroups: True if want search in the subgroups @@ -952,7 +849,7 @@ class KeycloakAdmin: if group["path"] == path: return group res = self.get_subgroups(group, path) - if res != None: + if res is not None: return res return None @@ -965,20 +862,20 @@ class KeycloakAdmin: :param skip_exists: If true then do not raise an error if it already exists GroupRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation - :return: Group ID for newly created group, otherwise None + :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( - URL_ADMIN_GROUPS.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_GROUPS.format(**params_path), data=json.dumps(payload) ) else: params_path = {"realm-name": self.realm_name, "id": parent} data_raw = self.raw_post( - URL_ADMIN_GROUP_CHILD.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_GROUP_CHILD.format(**params_path), data=json.dumps(payload) ) raise_error_from_response( @@ -998,13 +895,15 @@ class KeycloakAdmin: :param payload: GroupRepresentation with updated information. GroupRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation :return: Http response """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_put(URL_ADMIN_GROUP.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_GROUP.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def group_set_permissions(self, group_id, enabled=True): @@ -1018,7 +917,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_put( - URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), + urls_patterns.URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), data=json.dumps({"enabled": enabled}), ) return raise_error_from_response(data_raw, KeycloakPutError) @@ -1033,7 +932,9 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} - data_raw = self.raw_put(URL_ADMIN_USER_GROUP.format(**params_path), data=None) + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_USER_GROUP.format(**params_path), data=None + ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def group_user_remove(self, user_id, group_id): @@ -1046,7 +947,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} - data_raw = self.raw_delete(URL_ADMIN_USER_GROUP.format(**params_path)) + 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): @@ -1058,7 +959,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_delete(URL_ADMIN_GROUP.format(**params_path)) + 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): @@ -1066,13 +967,13 @@ class KeycloakAdmin: Returns a list of clients belonging to the realm ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response (ClientRepresentation) """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_CLIENTS.format(**params_path)) + 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): @@ -1080,14 +981,14 @@ class KeycloakAdmin: Get representation of the client ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :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(URL_ADMIN_CLIENT.format(**params_path)) + 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): @@ -1096,7 +997,7 @@ class KeycloakAdmin: This is required for further actions against this client. :param client_name: name in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: client_id (uuid as string) """ @@ -1113,12 +1014,14 @@ class KeycloakAdmin: Get authorization json from client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_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(URL_ADMIN_CLIENT_AUTHZ_SETTINGS.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SETTINGS.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def create_client_authz_resource(self, client_id, payload, skip_exists=False): @@ -1126,9 +1029,9 @@ class KeycloakAdmin: Create resources of client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :param payload: ResourceRepresentation - https://www.keycloak.org/docs-api/12.0/rest-api/index.html#_resourcerepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_resourcerepresentation :return: Keycloak server response """ @@ -1136,7 +1039,8 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -1147,12 +1051,14 @@ class KeycloakAdmin: Get resources from client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_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(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def create_client_authz_role_based_policy(self, client_id, payload, skip_exists=False): @@ -1160,28 +1066,30 @@ class KeycloakAdmin: Create role-based policy of client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :param payload: No Document - payload example: - payload={ - "type": "role", - "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", - "name": "Policy-1", - "roles": [ - { - "id": id - } - ] - } - :return: Keycloak server response + + Payload example:: + + payload={ + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "name": "Policy-1", + "roles": [ + { + "id": id + } + ] + } + """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY.format(**params_path), + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY.format(**params_path), data=json.dumps(payload), ) return raise_error_from_response( @@ -1193,29 +1101,31 @@ class KeycloakAdmin: Create resource-based permission of client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :param payload: PolicyRepresentation - https://www.keycloak.org/docs-api/12.0/rest-api/index.html#_policyrepresentation - payload example: - payload={ - "type": "resource", - "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", - "name": "Permission-Name", - "resources": [ - resource_id - ], - "policies": [ - policy_id - ] - + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_policyrepresentation :return: Keycloak server response + + Payload example:: + + payload={ + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "name": "Permission-Name", + "resources": [ + resource_id + ], + "policies": [ + policy_id + ] + """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION.format(**params_path), + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION.format(**params_path), data=json.dumps(payload), ) return raise_error_from_response( @@ -1227,12 +1137,12 @@ class KeycloakAdmin: Get scopes from client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_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(URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path)) + 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): @@ -1240,12 +1150,14 @@ class KeycloakAdmin: Get permissions from client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_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(URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_policies(self, client_id): @@ -1253,12 +1165,14 @@ class KeycloakAdmin: Get policies from client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_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(URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_service_account_user(self, client_id): @@ -1266,12 +1180,14 @@ class KeycloakAdmin: Get service account user from client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_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(URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def create_client(self, payload, skip_exists=False): @@ -1293,7 +1209,9 @@ class KeycloakAdmin: return client_id params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(payload) + ) raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) @@ -1310,7 +1228,9 @@ class KeycloakAdmin: :return: Http response """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_put(URL_ADMIN_CLIENT.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_CLIENT.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_client(self, client_id): @@ -1318,14 +1238,14 @@ class KeycloakAdmin: Get representation of the client ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :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(URL_ADMIN_CLIENT.format(**params_path)) + 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): @@ -1333,17 +1253,19 @@ class KeycloakAdmin: Get content for given installation provider Related documentation: - https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_clients_resource + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource Possible provider_id list available in the ServerInfoRepresentation#clientInstallations - https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_serverinforepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_serverinforepresentation :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(URL_ADMIN_CLIENT_INSTALLATION_PROVIDER.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_INSTALLATION_PROVIDER.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) def get_realm_roles(self): @@ -1351,24 +1273,28 @@ class KeycloakAdmin: Get all roles for the realm or client RoleRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_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(URL_ADMIN_REALM_ROLES.format(**params_path)) + 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): + def get_realm_role_members(self, role_name, query=None): """ 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/11.0/rest-api/index.html#_roles_resource) + :param query: Additional Query parameters + (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_roles_resource) :return: Keycloak Server Response (UserRepresentation) """ + query = query or dict() params_path = {"realm-name": self.realm_name, "role-name": role_name} - return self.__fetch_all(URL_ADMIN_REALM_ROLES_MEMBERS.format(**params_path), query) + return self.__fetch_all( + urls_patterns.URL_ADMIN_REALM_ROLES_MEMBERS.format(**params_path), query + ) def get_client_roles(self, client_id): """ @@ -1377,13 +1303,13 @@ class KeycloakAdmin: :param client_id: id of client (not client-id) RoleRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_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, "id": client_id} - data_raw = self.raw_get(URL_ADMIN_CLIENT_ROLES.format(**params_path)) + 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): @@ -1395,12 +1321,12 @@ class KeycloakAdmin: :param role_name: role’s name (not id!) RoleRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :return: role_id """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} - data_raw = self.raw_get(URL_ADMIN_CLIENT_ROLE.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_role_id(self, client_id, role_name): @@ -1414,7 +1340,7 @@ class KeycloakAdmin: :param role_name: role’s name (not id!) RoleRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :return: role_id """ @@ -1441,7 +1367,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_role_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) ) raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -1449,24 +1375,6 @@ class KeycloakAdmin: _last_slash_idx = data_raw.headers["Location"].rindex("/") return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 - def update_client_role(self, client_role_id, role_name, payload): - """ - Update a client role - - RoleRepresentation - https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation - - :param client_role_id: id of client (not client-id) - :param role_name: role's name (not id!) - :param payload: RoleRepresentation - """ - params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} - data_raw = self.raw_put( - URL_ADMIN_CLIENT_ROLE.format(**params_path), - data=json.dumps(payload), - ) - return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) - def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): """ Add composite roles to client role @@ -1474,29 +1382,46 @@ class KeycloakAdmin: :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 + :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( - URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE.format(**params_path), + urls_patterns.URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE.format(**params_path), data=json.dumps(payload), ) 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 + + RoleRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation + + :param client_role_id: id of client (not client-id) + :param role_name: role's name (not id!) + :param payload: RoleRepresentation + """ + params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_CLIENT_ROLE.format(**params_path), data=json.dumps(payload) + ) + 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 RoleRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :param client_role_id: id of client (not client-id) - :param role_name: role’s name (not id!) + :param role_name: role's name (not id!) """ params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} - data_raw = self.raw_delete(URL_ADMIN_CLIENT_ROLE.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_CLIENT_ROLE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def assign_client_role(self, user_id, client_id, roles): @@ -1506,13 +1431,14 @@ class KeycloakAdmin: :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 + :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( - URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) @@ -1521,11 +1447,14 @@ class KeycloakAdmin: 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 ( see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_clients_resource) + :param query: Additional query parameters + (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} - return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_MEMBERS.format(**params_path), query) + return self.__fetch_all( + urls_patterns.URL_ADMIN_CLIENT_ROLE_MEMBERS.format(**params_path), query + ) def get_client_role_groups(self, client_id, role_name, **query): """ @@ -1537,7 +1466,9 @@ class KeycloakAdmin: :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} - return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_GROUPS.format(**params_path), query) + return self.__fetch_all( + urls_patterns.URL_ADMIN_CLIENT_ROLE_GROUPS.format(**params_path), query + ) def create_realm_role(self, payload, skip_exists=False): """ @@ -1555,7 +1486,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( - URL_ADMIN_REALM_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path), data=json.dumps(payload) ) raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -1569,11 +1500,13 @@ class KeycloakAdmin: :param role_name: role's name, not id! RoleRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :return: role_id """ params_path = {"realm-name": self.realm_name, "role-name": role_name} - data_raw = self.raw_get(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def update_realm_role(self, role_name, payload): @@ -1586,7 +1519,8 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_put( - URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) @@ -1598,7 +1532,9 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "role-name": role_name} - data_raw = self.raw_delete(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_composite_realm_roles_to_role(self, role_name, roles): @@ -1607,13 +1543,13 @@ class KeycloakAdmin: :param role_name: The name of the role :param roles: roles list or role (use RoleRepresentation) to be updated - :return Keycloak server response + :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( - URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), + urls_patterns.URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) @@ -1624,13 +1560,13 @@ class KeycloakAdmin: :param role_name: The name of the role :param roles: roles list or role (use RoleRepresentation) to be removed - :return Keycloak server response + :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( - URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), + urls_patterns.URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) @@ -1640,11 +1576,13 @@ class KeycloakAdmin: Get composite roles of the role :param role_name: The name of the role - :return Keycloak server response (array RoleRepresentation) + :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "role-name": role_name} - data_raw = self.raw_get(URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def assign_realm_roles(self, user_id, roles): @@ -1653,13 +1591,14 @@ class KeycloakAdmin: :param user_id: id of user :param roles: roles list or role (use RoleRepresentation) - :return Keycloak server response + :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( - URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_USER_REALM_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) @@ -1669,13 +1608,14 @@ class KeycloakAdmin: :param user_id: id of user :param roles: roles list or role (use RoleRepresentation) - :return Keycloak server response + :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( - URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_USER_REALM_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) @@ -1688,7 +1628,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_REALM_ROLES.format(**params_path)) + 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): @@ -1698,7 +1638,9 @@ class KeycloakAdmin: :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_REALM_ROLES_AVAILABLE.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_USER_REALM_ROLES_AVAILABLE.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_composite_realm_roles_of_user(self, user_id): @@ -1708,7 +1650,9 @@ class KeycloakAdmin: :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_REALM_ROLES_COMPOSITE.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_USER_REALM_ROLES_COMPOSITE.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def assign_group_realm_roles(self, group_id, roles): @@ -1717,13 +1661,14 @@ class KeycloakAdmin: :param group_id: id of groupp :param roles: roles list or role (use GroupRoleRepresentation) - :return Keycloak server response + :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( - URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) @@ -1733,13 +1678,14 @@ class KeycloakAdmin: :param group_id: id of group :param roles: roles list or role (use GroupRoleRepresentation) - :return Keycloak server response + :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( - URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) @@ -1751,7 +1697,7 @@ class KeycloakAdmin: :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_get(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def assign_group_client_roles(self, group_id, client_id, roles): @@ -1761,13 +1707,14 @@ class KeycloakAdmin: :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 + :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( - URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) @@ -1777,11 +1724,11 @@ class KeycloakAdmin: :param group_id: id of group :param client_id: id of client (not client-id) - :return Keycloak server response + :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} - data_raw = self.raw_get(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path)) + 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): @@ -1791,13 +1738,14 @@ class KeycloakAdmin: :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) + :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( - URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) @@ -1809,7 +1757,9 @@ class KeycloakAdmin: :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ - return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES, user_id, client_id) + return self._get_client_roles_of_user( + urls_patterns.URL_ADMIN_USER_CLIENT_ROLES, user_id, client_id + ) def get_available_client_roles_of_user(self, user_id, client_id): """ @@ -1820,7 +1770,7 @@ class KeycloakAdmin: :return: Keycloak server response (array RoleRepresentation) """ return self._get_client_roles_of_user( - URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id + urls_patterns.URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id ) def get_composite_client_roles_of_user(self, user_id, client_id): @@ -1832,7 +1782,7 @@ class KeycloakAdmin: :return: Keycloak server response (array RoleRepresentation) """ return self._get_client_roles_of_user( - URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id + urls_patterns.URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id ) def _get_client_roles_of_user(self, client_level_role_mapping_url, user_id, client_id): @@ -1852,7 +1802,8 @@ class KeycloakAdmin: 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_delete( - URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) @@ -1861,26 +1812,26 @@ class KeycloakAdmin: Get authentication flows. Returns all flow details AuthenticationFlowRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation :return: Keycloak server response (AuthenticationFlowRepresentation) """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_FLOWS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_FLOWS.format(**params_path)) 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/alias. Returns all flow details + Get one authentication flow by it's id. Returns all flow details AuthenticationFlowRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation :param flow_id: the id of a flow NOT it's alias :return: Keycloak server response (AuthenticationFlowRepresentation) """ params_path = {"realm-name": self.realm_name, "flow-id": flow_id} - data_raw = self.raw_get(URL_ADMIN_FLOWS_ALIAS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_FLOWS_ALIAS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def create_authentication_flow(self, payload, skip_exists=False): @@ -1888,22 +1839,25 @@ class KeycloakAdmin: Create a new authentication flow AuthenticationFlowRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation :param payload: AuthenticationFlowRepresentation - :param skip_exists: If true then do not raise an error if authentication flow already exists + :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(URL_ADMIN_FLOWS.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_FLOWS.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) 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 @@ -1912,7 +1866,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post( - URL_ADMIN_FLOWS_COPY.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_FLOWS_COPY.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -1921,13 +1875,13 @@ class KeycloakAdmin: Delete authentication flow AuthenticationInfoRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationinforepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationinforepresentation :param flow_id: authentication flow id :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": flow_id} - data_raw = self.raw_delete(URL_ADMIN_FLOW.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_FLOW.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_authentication_flow_executions(self, flow_alias): @@ -1938,7 +1892,7 @@ class KeycloakAdmin: :return: Response(json) """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.raw_get(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def update_authentication_flow_executions(self, payload, flow_alias): @@ -1946,7 +1900,7 @@ class KeycloakAdmin: Update an authentication flow execution AuthenticationExecutionInfoRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation :param payload: AuthenticationExecutionInfoRepresentation :param flow_alias: The flow alias @@ -1955,7 +1909,8 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_put( - URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[202, 204]) @@ -1964,13 +1919,13 @@ class KeycloakAdmin: Get authentication flow execution. AuthenticationExecutionInfoRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation :param execution_id: the execution ID :return: Response(json) """ params_path = {"realm-name": self.realm_name, "id": execution_id} - data_raw = self.raw_get(URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def create_authentication_flow_execution(self, payload, flow_alias): @@ -1978,7 +1933,7 @@ class KeycloakAdmin: Create an authentication flow execution AuthenticationExecutionInfoRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation :param payload: AuthenticationExecutionInfoRepresentation :param flow_alias: The flow alias @@ -1987,7 +1942,8 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post( - URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -1996,13 +1952,13 @@ class KeycloakAdmin: Delete authentication flow execution AuthenticationExecutionInfoRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation :param execution_id: keycloak client id (not oauth client-id) :return: Keycloak server response (json) """ params_path = {"realm-name": self.realm_name, "id": execution_id} - data_raw = self.raw_delete(URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False): @@ -2010,17 +1966,18 @@ class KeycloakAdmin: Create a new sub authentication flow for a given authentication flow AuthenticationFlowRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation :param payload: AuthenticationFlowRepresentation :param flow_alias: The flow alias - :param skip_exists: If true then do not raise an error if authentication flow already exists + :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( - URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -2034,7 +1991,7 @@ class KeycloakAdmin: :return: Response(json) """ params_path = {"realm-name": self.realm_name, "id": config_id} - data_raw = self.raw_get(URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def update_authenticator_config(self, payload, config_id): @@ -2042,7 +1999,7 @@ class KeycloakAdmin: Update an authenticator configuration. AuthenticatorConfigRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticatorconfigrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticatorconfigrepresentation :param payload: AuthenticatorConfigRepresentation :param config_id: Authenticator config id @@ -2050,21 +2007,24 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": config_id} data_raw = self.raw_put( - URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_authenticator_config(self, config_id): """ Delete a authenticator configuration. - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authentication_management_resource + 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(URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path)) + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def sync_users(self, storage_id, action): @@ -2080,40 +2040,43 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": storage_id} data_raw = self.raw_post( - URL_ADMIN_USER_STORAGE.format(**params_path), data=json.dumps(data), **params_query + urls_patterns.URL_ADMIN_USER_STORAGE.format(**params_path), + data=json.dumps(data), + **params_query ) return raise_error_from_response(data_raw, KeycloakPostError) def get_client_scopes(self): """ Get representation of the client scopes for the realm where we are connected to - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes + 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(URL_ADMIN_CLIENT_SCOPES.format(**params_path)) + 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 representation of the client scopes for the realm where we are connected to - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes + 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(URL_ADMIN_CLIENT_SCOPE.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def create_client_scope(self, payload, skip_exists=False): """ Create a client scope - ClientScopeRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes + ClientScopeRepresentation: + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes :param payload: ClientScopeRepresentation :param skip_exists: If true then do not raise an error if client scope already exists @@ -2122,7 +2085,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( - URL_ADMIN_CLIENT_SCOPES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_SCOPES.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -2132,7 +2095,8 @@ class KeycloakAdmin: """ Update a client scope - ClientScopeRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_client_scopes_resource + ClientScopeRepresentation: + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_client_scopes_resource :param client_scope_id: The id of the client scope :param payload: ClientScopeRepresentation @@ -2141,14 +2105,14 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_put( - URL_ADMIN_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def add_mapper_to_client_scope(self, client_scope_id, payload): """ Add a mapper to a client scope - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_create_mapper + 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 @@ -2158,7 +2122,8 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -2166,7 +2131,7 @@ class KeycloakAdmin: def delete_mapper_from_client_scope(self, client_scope_id, protocol_mppaer_id): """ Delete a mapper from a client scope - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_delete_mapper + 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 payload: ProtocolMapperRepresentation @@ -2179,13 +2144,15 @@ class KeycloakAdmin: "protocol-mapper-id": protocol_mppaer_id, } - data_raw = self.raw_delete(URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path)) + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path) + ) 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 - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_protocol_mappers_resource + 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 :param protocol_mapper_id: The id of the protocol mapper which exists in the client scope @@ -2201,7 +2168,8 @@ class KeycloakAdmin: } data_raw = self.raw_put( - URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) @@ -2213,7 +2181,9 @@ class KeycloakAdmin: :return: Keycloak server response """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def delete_default_default_client_scope(self, scope_id): @@ -2224,7 +2194,9 @@ class KeycloakAdmin: :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": scope_id} - data_raw = self.raw_delete(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path)) + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_default_default_client_scope(self, scope_id): @@ -2237,7 +2209,8 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": scope_id} payload = {"realm": self.realm_name, "clientScopeId": scope_id} data_raw = self.raw_put( - URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) @@ -2248,7 +2221,9 @@ class KeycloakAdmin: :return: Keycloak server response """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def delete_default_optional_client_scope(self, scope_id): @@ -2259,7 +2234,9 @@ class KeycloakAdmin: :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": scope_id} - data_raw = self.raw_delete(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path)) + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_default_optional_client_scope(self, scope_id): @@ -2272,14 +2249,15 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": scope_id} payload = {"realm": self.realm_name, "clientScopeId": scope_id} data_raw = self.raw_put( - URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def add_mapper_to_client(self, client_id, payload): """ Add a mapper to a client - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_create_mapper + 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 @@ -2289,7 +2267,8 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_PROTOCOL_MAPPERS.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_PROTOCOL_MAPPERS.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -2310,7 +2289,8 @@ class KeycloakAdmin: } data_raw = self.raw_put( - URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) @@ -2330,35 +2310,39 @@ class KeycloakAdmin: "protocol-mapper-id": client_mapper_id, } - data_raw = self.raw_delete(URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path)) + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path) + ) 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 - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_regeneratesecret + 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(URL_ADMIN_CLIENT_SECRETS.format(**params_path), data=None) + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_CLIENT_SECRETS.format(**params_path), data=None + ) return raise_error_from_response(data_raw, KeycloakPostError) def get_client_secrets(self, client_id): """ Get representation of the client secrets - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientsecret + 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(URL_ADMIN_CLIENT_SECRETS.format(**params_path)) + 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): @@ -2366,13 +2350,15 @@ class KeycloakAdmin: Return a list of components, filtered according to query parameters ComponentRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation :param query: Query parameters (optional) :return: components list """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_COMPONENTS.format(**params_path), data=None, **query) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_COMPONENTS.format(**params_path), data=None, **query + ) return raise_error_from_response(data_raw, KeycloakGetError) def create_component(self, payload): @@ -2380,7 +2366,7 @@ class KeycloakAdmin: Create a new component. ComponentRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation :param payload: ComponentRepresentation @@ -2389,7 +2375,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( - URL_ADMIN_COMPONENTS.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_COMPONENTS.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -2400,12 +2386,12 @@ class KeycloakAdmin: :param component_id: Component id ComponentRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation :return: ComponentRepresentation """ params_path = {"realm-name": self.realm_name, "component-id": component_id} - data_raw = self.raw_get(URL_ADMIN_COMPONENT.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_COMPONENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def update_component(self, component_id, payload): @@ -2414,13 +2400,13 @@ class KeycloakAdmin: :param component_id: Component id :param payload: ComponentRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation :return: Http response """ params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_put( - URL_ADMIN_COMPONENT.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_COMPONENT.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) @@ -2433,7 +2419,7 @@ class KeycloakAdmin: :return: Http response """ params_path = {"realm-name": self.realm_name, "component-id": component_id} - data_raw = self.raw_delete(URL_ADMIN_COMPONENT.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_COMPONENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_keys(self): @@ -2441,12 +2427,12 @@ class KeycloakAdmin: Return a list of keys, filtered according to query parameters KeysMetadataRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_key_resource + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_key_resource :return: keys list """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_KEYS.format(**params_path), data=None) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_KEYS.format(**params_path), data=None) return raise_error_from_response(data_raw, KeycloakGetError) def get_events(self, query=None): @@ -2454,12 +2440,14 @@ class KeycloakAdmin: Return a list of events, filtered according to query parameters EventRepresentation array - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_eventrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_eventrepresentation :return: events list """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_EVENTS.format(**params_path), data=None, **query) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_EVENTS.format(**params_path), data=None, **query + ) return raise_error_from_response(data_raw, KeycloakGetError) def set_events(self, payload): @@ -2467,12 +2455,14 @@ class KeycloakAdmin: Set realm events configuration RealmEventsConfigRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmeventsconfigrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmeventsconfigrepresentation :return: Http response """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_put(URL_ADMIN_EVENTS.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_EVENTS.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def raw_get(self, *args, **kwargs): @@ -2518,8 +2508,8 @@ class KeycloakAdmin: """ Calls 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. + If auto_refresh is set for *delete* and *access_token* is expired, + it will refresh the token and try *delete* once more. """ r = self.connection.raw_delete(*args, **kwargs) if "delete" in self.auto_refresh_token and r.status_code == 401: @@ -2551,7 +2541,7 @@ class KeycloakAdmin: self.realm_name = self.user_realm_name if self.username and self.password: - self._token = self.keycloak_openid.token( + self.token = self.keycloak_openid.token( self.username, self.password, grant_type=grant_type, totp=self.totp ) @@ -2560,14 +2550,14 @@ class KeycloakAdmin: "Content-Type": "application/json", } else: - self._token = None + self.token = None headers = {} if self.custom_headers is not None: # merge custom headers to main headers headers.update(self.custom_headers) - self._connection = ConnectionManager( + self.connection = ConnectionManager( base_url=self.server_url, headers=headers, timeout=60, verify=self.verify ) @@ -2600,22 +2590,22 @@ class KeycloakAdmin: :param client_id: id of client UserSessionRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_usersessionrepresentation + http://www.keycloak.org/docs-api/18.0/rest-api/index.html#_usersessionrepresentation :return: UserSessionRepresentation """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_get(URL_ADMIN_CLIENT_ALL_SESSIONS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ALL_SESSIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_sessions_stats(self): """ Get current session count for all clients with active sessions - https://www.keycloak.org/docs-api/16.1/rest-api/index.html#_getclientsessionstats + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientsessionstats :return: Dict of clients and session count """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(self.URL_ADMIN_CLIENT_SESSION_STATS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SESSION_STATS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 34d8514..7450f2e 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -124,7 +124,10 @@ URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = ( "admin/realms/{realm-name}/roles/{role-name}/composites" ) -URL_ADMIN_REALM_EXPORT = "admin/realms/{realm-name}/partial-export?exportClients={export-clients}&exportGroupsAndRoles={export-groups-and-roles}" +URL_ADMIN_REALM_EXPORT = ( + "admin/realms/{realm-name}/partial-export?exportClients={export-clients}&" + + "exportGroupsAndRoles={export-groups-and-roles}" +) URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES = URL_ADMIN_REALM + "/default-default-client-scopes" URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE = URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES + "/{id}" From 3636de017762b5ca48c5bdb9ccc02894d6bd8014 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 12:57:49 +0200 Subject: [PATCH 6/9] docs: added autoapi to docs --- .gitignore | 3 +- README.md | 70 +++++----- docs/source/conf.py | 19 ++- docs/source/index.rst | 303 +---------------------------------------- docs/source/readme.rst | 1 + 5 files changed, 58 insertions(+), 338 deletions(-) create mode 100644 docs/source/readme.rst diff --git a/.gitignore b/.gitignore index 4c8d46d..24f085b 100644 --- a/.gitignore +++ b/.gitignore @@ -104,4 +104,5 @@ ENV/ main.py main2.py s3air-authz-config.json -.vscode \ No newline at end of file +.vscode +_build \ No newline at end of file diff --git a/README.md b/README.md index 68a2dc5..01d6e0f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ -[![CircleCI](https://circleci.com/gh/marcospereirampj/python-keycloak/tree/master.svg?style=svg)](https://circleci.com/gh/marcospereirampj/python-keycloak/tree/master) +[![CircleCI](https://github.com/marcospereirampj/python-keycloak/actions/workflows/daily.yaml/badge.svg)](https://github.com/marcospereirampj/python-keycloak/) [![Documentation Status](https://readthedocs.org/projects/python-keycloak/badge/?version=latest)](http://python-keycloak.readthedocs.io/en/latest/?badge=latest) - -Python Keycloak -==================== +# Python Keycloak For review- see https://github.com/marcospereirampj/python-keycloak @@ -13,24 +11,27 @@ For review- see https://github.com/marcospereirampj/python-keycloak ### Via Pypi Package: -``` $ pip install python-keycloak ``` +`$ pip install python-keycloak` ### Manually -``` $ python setup.py install ``` +`$ python setup.py install` ## Dependencies python-keycloak depends on: -* Python 3 -* [requests](https://requests.readthedocs.io) -* [python-jose](http://python-jose.readthedocs.io/en/latest/) +- Python 3 +- [requests](https://requests.readthedocs.io) +- [python-jose](http://python-jose.readthedocs.io/en/latest/) +- [urllib3](https://urllib3.readthedocs.io/en/stable/) ### Tests Dependencies -* unittest -* [httmock](https://github.com/patrys/httmock) +- [tox](https://tox.readthedocs.io/) +- [pytest](https://docs.pytest.org/en/latest/) +- [pytest-cov](https://github.com/pytest-dev/pytest-cov) +- [wheel](https://github.com/pypa/wheel) ## Bug reports @@ -43,18 +44,19 @@ The documentation for python-keycloak is available on [readthedocs](http://pytho ## Contributors -* [Agriness Team](http://www.agriness.com/pt/) -* [Marcos Pereira](marcospereira.mpj@gmail.com) -* [Martin Devlin](https://bitbucket.org/devlinmpearson/) -* [Shon T. Urbas](https://bitbucket.org/surbas/) -* [Markus Spanier](https://bitbucket.org/spanierm/) -* [Remco Kranenburg](https://bitbucket.org/Remco47/) -* [Armin](https://bitbucket.org/arminfelder/) -* [njordr](https://bitbucket.org/njordr/) -* [Josha Inglis](https://bitbucket.org/joshainglis/) -* [Alex](https://bitbucket.org/alex_zel/) -* [Ewan Jone](https://bitbucket.org/kisamoto/) -* [Lukas Martini](https://github.com/lutoma) +- [Agriness Team](http://www.agriness.com/pt/) +- [Marcos Pereira](marcospereira.mpj@gmail.com) +- [Martin Devlin](https://bitbucket.org/devlinmpearson/) +- [Shon T. Urbas](https://bitbucket.org/surbas/) +- [Markus Spanier](https://bitbucket.org/spanierm/) +- [Remco Kranenburg](https://bitbucket.org/Remco47/) +- [Armin](https://bitbucket.org/arminfelder/) +- [njordr](https://bitbucket.org/njordr/) +- [Josha Inglis](https://bitbucket.org/joshainglis/) +- [Alex](https://bitbucket.org/alex_zel/) +- [Ewan Jone](https://bitbucket.org/kisamoto/) +- [Lukas Martini](https://github.com/lutoma) +- [Adamatics](https://www.adamatics.com) ## Usage @@ -119,13 +121,13 @@ keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", user_realm_name="only_if_other_realm_than_master", client_secret_key="client-secret", verify=True) - -# Add user + +# Add user new_user = keycloak_admin.create_user({"email": "example@example.com", "username": "example@example.com", "enabled": True, "firstName": "Example", - "lastName": "Example"}) + "lastName": "Example"}) # Add user and raise exception if username already exists # exist_ok currently defaults to True for backwards compatibility reasons @@ -135,8 +137,8 @@ new_user = keycloak_admin.create_user({"email": "example@example.com", "firstName": "Example", "lastName": "Example"}, exist_ok=False) - -# Add user and set password + +# Add user and set password new_user = keycloak_admin.create_user({"email": "example@example.com", "username": "example@example.com", "enabled": True, @@ -144,7 +146,7 @@ new_user = keycloak_admin.create_user({"email": "example@example.com", "lastName": "Example", "credentials": [{"value": "secret","type": "password",}]}) -# Add user and specify a locale +# Add user and specify a locale new_user = keycloak_admin.create_user({"email": "example@example.fr", "username": "example@example.fr", "enabled": True, @@ -152,7 +154,7 @@ new_user = keycloak_admin.create_user({"email": "example@example.fr", "lastName": "Example", "attributes": { "locale": ["fr"] - }) + }) # User counter count_users = keycloak_admin.users_count() @@ -167,7 +169,7 @@ user_id_keycloak = keycloak_admin.get_user_id("example@example.com") user = keycloak_admin.get_user("user-id-keycloak") # Update User -response = keycloak_admin.update_user(user_id="user-id-keycloak", +response = keycloak_admin.update_user(user_id="user-id-keycloak", payload={'firstName': 'Example Update'}) # Update User Password @@ -181,7 +183,7 @@ credential = keycloak_admin.get_credential(user_id='user_id', credential_id='cre # Delete User Credential response = keycloak_admin.delete_credential(user_id='user_id', credential_id='credential_id') - + # Delete User response = keycloak_admin.delete_user(user_id="user-id-keycloak") @@ -189,7 +191,7 @@ response = keycloak_admin.delete_user(user_id="user-id-keycloak") consents = keycloak_admin.consents_user(user_id="user-id-keycloak") # Send User Action -response = keycloak_admin.send_update_account(user_id="user-id-keycloak", +response = keycloak_admin.send_update_account(user_id="user-id-keycloak", payload=json.dumps(['UPDATE_PASSWORD'])) # Send Verify Email @@ -260,7 +262,7 @@ group = keycloak_admin.create_group({"name": "Example Group"}) # Get all groups groups = keycloak_admin.get_groups() -# Get group +# Get group group = keycloak_admin.get_group(group_id='group_id') # Get group by name diff --git a/docs/source/conf.py b/docs/source/conf.py index 23e9bbf..7d0b77d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -21,6 +21,7 @@ # import sys # sys.path.insert(0, os.path.abspath('.')) import sphinx_rtd_theme +from keycloak import __version__ # -- General configuration ------------------------------------------------ @@ -36,8 +37,16 @@ extensions = [ "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.viewcode", + "m2r2", + "autoapi.extension", ] +autoapi_type = "python" +autoapi_dirs = ["../../keycloak"] +autoapi_root = "reference" +autoapi_keep_files = False +autoapi_add_toctree_entry = False + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -60,9 +69,9 @@ author = "Marcos Pereira" # built documents. # # The short X.Y version. -version = "0.27.1" +version = __version__ # The full version, including alpha/beta/rc tags. -release = "0.27.1" +release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -103,7 +112,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] +# html_static_path = ["_static"] html_use_smartypants = False @@ -160,7 +169,7 @@ latex_documents = [ "python-keycloak Documentation", "Marcos Pereira", "manual", - ) + ), ] @@ -185,5 +194,5 @@ texinfo_documents = [ "python-keycloak", "One line description of project.", "Miscellaneous", - ) + ), ] diff --git a/docs/source/index.rst b/docs/source/index.rst index 6675352..6dff08d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,305 +3,12 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. +.. image:: https://readthedocs.org/projects/adamatics-keycloak/badge/?version=latest + :target: https://adamatics-keycloak.readthedocs.io/en/latest/?badge=latest +.. mdinclude:: ../../README.md .. toctree:: :maxdepth: 2 :caption: Contents: - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - -.. image:: https://readthedocs.org/projects/python-keycloak/badge/?version=latest - :target: http://python-keycloak.readthedocs.io/en/latest/?badge=latest - - -Welcome to python-keycloak's documentation! -=========================================== - -**python-keycloak** is a Python package providing access to the Keycloak API. - -Installation -================== - -Via Pypi Package:: - - $ pip install python-keycloak - -Manually:: - - $ python setup.py install - -Dependencies -================== - -python-keycloak depends on: - -* Python 3 -* `requests `_ -* `python-jose `_ - -Tests Dependencies ------------------- - -* unittest -* `httmock `_ - -Bug reports -================== - -Please report bugs and feature requests at -`https://github.com/marcospereirampj/python-keycloak/issues `_ - -Documentation -================== - -The documentation for python-keycloak is available on `readthedocs `_. - -Contributors -================== - -* `Agriness Team `_ -* `Marcos Pereira `_ -* `Martin Devlin `_ -* `Shon T. Urbas `_ -* `Markus Spanier `_ -* `Remco Kranenburg `_ -* `Armin `_ -* `Njordr `_ -* `Josha Inglis `_ -* `Alex `_ -* `Ewan Jone `_ - -Usage -===== - -Main methods:: - - # KEYCLOAK OPENID - - from keycloak import KeycloakOpenID - - # Configure client - keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", - client_id="example_client", - realm_name="example_realm", - client_secret_key="secret", - verify=True) - - # Optionally, you can pass custom headers that will be added to all HTTP calls - # keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", - # client_id="example_client", - # realm_name="example_realm", - # client_secret_key="secret", - # verify=True, - # custom_headers={'CustomHeader': 'value'}) - - # Optionally, you can pass proxies as well that will be used in all HTTP calls. See requests documentation for more details_ - # `requests-proxies `_. - # keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", - # client_id="example_client", - # realm_name="example_realm", - # client_secret_key="secret", - # verify=True, - # proxies={'http': 'http://10.10.1.10:3128', 'https': 'http://10.10.1.10:1080'}) - - # Get WellKnow - config_well_know = keycloak_openid.well_know() - - # Get Token - token = keycloak_openid.token("user", "password") - token = keycloak_openid.token("user", "password", totp="012345") - - # Get Userinfo - userinfo = keycloak_openid.userinfo(token['access_token']) - - # Refresh token - token = keycloak_openid.refresh_token(token['refresh_token']) - - # Logout - keycloak_openid.logout(token['refresh_token']) - - # Get Certs - certs = keycloak_openid.certs() - - # Get RPT (Entitlement) - token = keycloak_openid.token("user", "password") - rpt = keycloak_openid.entitlement(token['access_token'], "resource_id") - - # Instropect RPT - token_rpt_info = keycloak_openid.introspect(keycloak_openid.introspect(token['access_token'], rpt=rpt['rpt'], - token_type_hint="requesting_party_token")) - - # Introspect Token - token_info = keycloak_openid.introspect(token['access_token'])) - - # Decode Token - KEYCLOAK_PUBLIC_KEY = "secret" - options = {"verify_signature": True, "verify_aud": True, "verify_exp": True} - token_info = keycloak_openid.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options) - - # Get permissions by token - token = keycloak_openid.token("user", "password") - keycloak_openid.load_authorization_config("example-authz-config.json") - policies = keycloak_openid.get_policies(token['access_token'], method_token_info='decode', key=KEYCLOAK_PUBLIC_KEY) - permissions = keycloak_openid.get_permissions(token['access_token'], method_token_info='introspect') - - # KEYCLOAK ADMIN - - from keycloak import KeycloakAdmin - - keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", - username='example-admin', - password='secret', - realm_name="example_realm", - verify=True) - - # Optionally, you can pass custom headers that will be added to all HTTP calls - #keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", - # username='example-admin', - # password='secret', - # realm_name="example_realm", - # verify=True, - # custom_headers={'CustomHeader': 'value'}) - # - # You can also authenticate with client_id and client_secret - #keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", - # client_id="example_client", - # client_secret_key="secret", - # realm_name="example_realm", - # verify=True, - # custom_headers={'CustomHeader': 'value'}) - - # Add user - new_user = keycloak_admin.create_user({"email": "example@example.com", - "username": "example@example.com", - "enabled": True, - "firstName": "Example", - "lastName": "Example", - "realmRoles": ["user_default", ], - "attributes": {"example": "1,2,3,3,"}}) - - - # Add user and set password - new_user = keycloak_admin.create_user({"email": "example@example.com", - "username": "example@example.com", - "enabled": True, - "firstName": "Example", - "lastName": "Example", - "credentials": [{"value": "secret","type": "password",}], - "realmRoles": ["user_default", ], - "attributes": {"example": "1,2,3,3,"}}) - - # User counter - count_users = keycloak_admin.users_count() - - # Get users Returns a list of users, filtered according to query parameters - users = keycloak_admin.get_users({}) - - # Get user ID from name - user-id-keycloak = keycloak_admin.get_user_id("example@example.com") - - # Get User - user = keycloak_admin.get_user("user-id-keycloak") - - # Update User - response = keycloak_admin.update_user(user_id="user-id-keycloak", - payload={'firstName': 'Example Update'}) - - # Update User Password - response = set_user_password(user_id="user-id-keycloak", password="secret", temporary=True) - - # Delete User - response = keycloak_admin.delete_user(user_id="user-id-keycloak") - - # Get consents granted by the user - consents = keycloak_admin.consents_user(user_id="user-id-keycloak") - - # Send User Action - response = keycloak_admin.send_update_account(user_id="user-id-keycloak", - payload=json.dumps(['UPDATE_PASSWORD'])) - - # Send Verify Email - response = keycloak_admin.send_verify_email(user_id="user-id-keycloak") - - # Get sessions associated with the user - sessions = keycloak_admin.get_sessions(user_id="user-id-keycloak") - - # Get themes, social providers, auth providers, and event listeners available on this server - server_info = keycloak_admin.get_server_info() - - # Get clients belonging to the realm Returns a list of clients belonging to the realm - clients = keycloak_admin.get_clients() - - # Get client - id (not client-id) from client by name - client_id=keycloak_admin.get_client_id("my-client") - - # Get representation of the client - id of client (not client-id) - client = keycloak_admin.get_client(client_id="client_id") - - # Get all roles for the realm or client - realm_roles = keycloak_admin.get_realm_roles() - - # Get all roles for the client - client_roles = keycloak_admin.get_client_roles(client_id="client_id") - - # Get client role - role = keycloak_admin.get_client_role(client_id="client_id", role_name="role_name") - - # Warning: Deprecated - # Get client role id from name - role_id = keycloak_admin.get_client_role_id(client_id="client_id", role_name="test") - - # Create client role - keycloak_admin.create_client_role(client_id="client_id", {'name': 'roleName', 'clientRole': True}) - - # Get client role id from name - role_id = keycloak_admin.get_client_role_id(client_id=client_id, role_name="test") - - # Get all roles for the realm or client - realm_roles = keycloak_admin.get_roles() - - # Assign client role to user. Note that BOTH role_name and role_id appear to be required. - keycloak_admin.assign_client_role(client_id="client_id", user_id="user_id", role_id="role_id", role_name="test") - - # Assign realm roles to user. Note that BOTH role_name and role_id appear to be required. - keycloak_admin.assign_realm_roles(client_id="client_id", user_id="user_id", roles=[{"roles_representation"}]) - - # Delete realm roles of user. Note that BOTH role_name and role_id appear to be required. - keycloak_admin.deletes_realm_roles_of_user(user_id="user_id", roles=[{"roles_representation"}]) - - # Create new group - group = keycloak_admin.create_group(name="Example Group") - - # Get all groups - groups = keycloak_admin.get_groups() - - # Get group - group = keycloak_admin.get_group(group_id='group_id') - - # Get group by path - group = keycloak_admin.get_group_by_path(path='/group/subgroup', search_in_subgroups=True) - - # Function to trigger user sync from provider - sync_users(storage_id="storage_di", action="action") - - # List public RSA keys - components = keycloak_admin.keys - - # List all keys - components = keycloak_admin.get_components(query={"parent":"example_realm", "type":"org.keycloak.keys.KeyProvider"}) - - # Create a new RSA key - component = keycloak_admin.create_component({"name":"rsa-generated","providerId":"rsa-generated","providerType":"org.keycloak.keys.KeyProvider","parentId":"example_realm","config":{"priority":["100"],"enabled":["true"],"active":["true"],"algorithm":["RS256"],"keySize":["2048"]}}) - - # Update the key - component_details['config']['active'] = ["false"] - keycloak_admin.update_component(component['id']) - - # Delete the key - keycloak_admin.delete_component(component['id']) - + readme + reference/keycloak/index diff --git a/docs/source/readme.rst b/docs/source/readme.rst new file mode 100644 index 0000000..3bd447c --- /dev/null +++ b/docs/source/readme.rst @@ -0,0 +1 @@ +.. mdinclude:: ../../README.md From 51cb44fe3aca1ed91e4d2da0ecbc2498f8217bc9 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 12:59:25 +0200 Subject: [PATCH 7/9] refactor: isort conf.py --- docs/source/conf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7d0b77d..403d465 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -21,6 +21,7 @@ # import sys # sys.path.insert(0, os.path.abspath('.')) import sphinx_rtd_theme + from keycloak import __version__ # -- General configuration ------------------------------------------------ @@ -169,7 +170,7 @@ latex_documents = [ "python-keycloak Documentation", "Marcos Pereira", "manual", - ), + ) ] @@ -194,5 +195,5 @@ texinfo_documents = [ "python-keycloak", "One line description of project.", "Miscellaneous", - ), + ) ] From fa9e56ef42102f34986ead5365cc7854e4c1ca63 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 14:45:06 +0200 Subject: [PATCH 8/9] feat: added authenticator providers getters --- keycloak/keycloak_admin.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 7b92d80..df53859 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1983,6 +1983,34 @@ class KeycloakAdmin: data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) + def get_authenticator_providers(self): + """ + Get authenticator providers list. + + :return: Response(json) + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_AUTHENTICATOR_PROVIDERS.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_authenticator_provider_config_description(self, provider_id): + """ + Get authenticator's provider configuration description. + + AuthenticatorConfigInfoRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticatorconfiginforepresentation + + :param provider_id: Provider Id + :return: AuthenticatorConfigInfoRepresentation + """ + params_path = {"realm-name": self.realm_name, "provider-id": provider_id} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_AUTHENTICATOR_CONFIG_DESCRIPTION.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + def get_authenticator_config(self, config_id): """ Get authenticator configuration. Returns all configuration details. From 7ae0442370a86c821e1bf8f2e2f4c2f6abe55e16 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 14:45:21 +0200 Subject: [PATCH 9/9] test: test authenticator and configurations --- keycloak/urls_patterns.py | 6 ++++++ tests/test_keycloak_admin.py | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 7450f2e..071c733 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -148,6 +148,12 @@ URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION = ( URL_ADMIN_FLOWS_EXECUTIONS_FLOW = ( "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" ) +URL_ADMIN_AUTHENTICATOR_PROVIDERS = ( + "admin/realms/{realm-name}/authentication/authenticator-providers" +) +URL_ADMIN_AUTHENTICATOR_CONFIG_DESCRIPTION = ( + "admin/realms/{realm-name}/authentication/config-description/{provider-id}" +) URL_ADMIN_AUTHENTICATOR_CONFIG = "admin/realms/{realm-name}/authentication/config/{id}" URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 29d3b15..6b04af7 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1199,3 +1199,43 @@ def test_auth_flows(admin: KeycloakAdmin, realm: str): with pytest.raises(KeycloakDeleteError) as err: admin.delete_authentication_flow(flow_id=flow_id) assert err.match('404: b\'{"error":"Could not find flow with id"}\'') + + +def test_authentication_configs(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Test list of auth providers + res = admin.get_authenticator_providers() + assert len(res) == 39 + + res = admin.get_authenticator_provider_config_description(provider_id="auth-cookie") + assert res == { + "helpText": "Validates the SSO cookie set by the auth server.", + "name": "Cookie", + "properties": [], + "providerId": "auth-cookie", + } + + # Test authenticator config + # Currently unable to find a sustainable way to fetch the config id, + # therefore testing only failures + with pytest.raises(KeycloakGetError) as err: + admin.get_authenticator_config(config_id="bad") + assert err.match('404: b\'{"error":"Could not find authenticator config"}\'') + + with pytest.raises(KeycloakPutError) as err: + admin.update_authenticator_config(payload=dict(), config_id="bad") + assert err.match('404: b\'{"error":"Could not find authenticator config"}\'') + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_authenticator_config(config_id="bad") + assert err.match('404: b\'{"error":"Could not find authenticator config"}\'') + + +def test_sync_users(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Only testing the error message + with pytest.raises(KeycloakPostError) as err: + admin.sync_users(storage_id="does-not-exist", action="triggerFullSync") + assert err.match('404: b\'{"error":"Could not find component"}\'')