diff --git a/.editorconfig b/.editorconfig index a83363b..8ae05aa 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,15 +1,54 @@ -[*] -indent_style = space +# Generated from: +# https://github.com/plone/meta/tree/master/config/default +# See the inline comments on how to expand/tweak this configuration file +# +# EditorConfig Configuration file, for more details see: +# http://EditorConfig.org +# EditorConfig is a convention description, that could be interpreted +# by multiple editors to enforce common coding conventions for specific +# file types + +# top-most EditorConfig file: +# Will ignore other EditorConfig files in Home directory or upper tree level. +root = true + + +[*] # For All Files +# Unix-style newlines with a newline ending every file end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true +# Set default charset charset = utf-8 +# Indent style default +indent_style = space +# Max Line Length - a hard line wrap, should be disabled +max_line_length = off -[{*.py,*.cfg}] +[*.{py,cfg,ini}] +# 4 space indentation indent_size = 4 -[{*.html,*.dtml,*.pt,*.zpt,*.xml,*.zcml,*.js}] +[*.{yml,zpt,pt,dtml,zcml}] +# 2 space indentation indent_size = 2 -[Makefile] +[*.{json,jsonl,js,jsx,ts,tsx,css,less,scss,html}] # Frontend development +# 2 space indentation +indent_size = 2 +max_line_length = 80 + +[{Makefile,.gitmodules}] +# Tab indentation (no size specified, but view as 4 spaces) indent_style = tab +indent_size = unset +tab_width = unset + + +## +# Add extra configuration options in .meta.toml: +# [editorconfig] +# extra_lines = """ +# _your own configuration lines_ +# """ +## diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..7ef4f64 --- /dev/null +++ b/.flake8 @@ -0,0 +1,22 @@ +# Generated from: +# https://github.com/plone/meta/tree/master/config/default +# See the inline comments on how to expand/tweak this configuration file +[flake8] +doctests = 1 +ignore = + # black takes care of line length + E501, + # black takes care of where to break lines + W503, + # black takes care of spaces within slicing (list[:]) + E203, + # black takes care of spaces after commas + E231, + +## +# Add extra configuration options in .meta.toml: +# [flake8] +# extra_lines = """ +# _your own configuration lines_ +# """ +## diff --git a/.github/workflows/bandit.yml b/.github/workflows/bandit.yml deleted file mode 100644 index 1d24a0f..0000000 --- a/.github/workflows/bandit.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Security check - Bandit - -on: push - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Security check - Bandit - uses: ioggstream/bandit-report-artifacts@v0.0.2 - with: - project_path: src - ignore_failure: true - - # This is optional - - name: Security check report artifacts - uses: actions/upload-artifact@v1 - with: - name: Security report - path: output/security_report.txt diff --git a/.github/workflows/meta.yml b/.github/workflows/meta.yml new file mode 100644 index 0000000..c9db54b --- /dev/null +++ b/.github/workflows/meta.yml @@ -0,0 +1,66 @@ +# Generated from: +# https://github.com/plone/meta/tree/master/config/default +# See the inline comments on how to expand/tweak this configuration file +name: Meta +on: + push: + branches: + - master + - main + pull_request: + branches: + - master + - main + workflow_dispatch: + +## +# To set environment variables for all jobs, add in .meta.toml: +# [github] +# env = """ +# debug: 1 +# image-name: 'org/image' +# image-tag: 'latest' +# """ +## + +jobs: + qa: + uses: plone/meta/.github/workflows/qa.yml@1.0.0 + test: + uses: plone/meta/.github/workflows/test.yml@1.0.0 + coverage: + uses: plone/meta/.github/workflows/coverage.yml@1.0.0 + dependencies: + uses: plone/meta/.github/workflows/dependencies.yml@1.0.0 + release_ready: + uses: plone/meta/.github/workflows/release_ready.yml@1.0.0 + +## +# To modify the list of default jobs being created add in .meta.toml: +# [github] +# jobs = [ +# "qa", +# "test", +# "coverage", +# "dependencies", +# "release_ready", +# "circular", +# ] +## + +## +# To request that some OS level dependencies get installed +# when running tests/coverage jobs, add in .meta.toml: +# [github] +# os_dependencies = "git libxml2 libxslt" +## + + +## +# Specify additional jobs in .meta.toml: +# [github] +# extra_lines = """ +# another: +# uses: org/repo/.github/workflows/file.yml@main +# """ +## diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 48b3774..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: tests - -on: - push: - branches: [ main ] - pull_request: - -jobs: - build: - strategy: - matrix: - config: - # [Python version, tox env] - #- ["2.7", "plone43-py27"] - #- ["2.7", "plone50-py27"] - #- ["2.7", "plone51-py27"] - #- ["2.7", "plone52-py27"] - - ["3.7", "plone52-py37"] - - ["3.8", "plone52-py38"] - - ["3.8", "plone60-py38"] - - ["3.9", "plone60-py39"] - - ["3.10", "plone60-py310"] - - ["3.11", "plone60-py311"] - runs-on: ubuntu-latest - name: ${{ matrix.config[1] }} - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.config[0] }} - - name: Pip cache - uses: actions/cache@v3 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ matrix.config[0] }}-${{ hashFiles('setup.*', 'tox.ini') }} - restore-keys: | - ${{ runner.os }}-pip-${{ matrix.config[0] }}- - ${{ runner.os }}-pip- - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox - - name: Test - run: tox -e ${{ matrix.config[1] }} - - name: Coveralls - uses: AndreMiras/coveralls-python-action@develop - with: - parallel: true - flag-name: ${{ matrix.config[1] }} - - coveralls_finish: - needs: build - runs-on: ubuntu-latest - steps: - - name: Coveralls Finished - uses: AndreMiras/coveralls-python-action@develop - with: - parallel-finished: true - diff --git a/.gitignore b/.gitignore index 7f27cd1..c16e8e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,35 +1,56 @@ -.coverage -coverage.json +# Generated from: +# https://github.com/plone/meta/tree/master/config/default +# See the inline comments on how to expand/tweak this configuration file +# python related *.egg-info -*.log +*.pyc +*.pyo + +# translation related *.mo -*.py? -*.swp -# dirs + +# tools related +build/ +.coverage +.*project +coverage.xml +dist/ +docs/_build +__pycache__/ +.tox +.vscode/ +node_modules/ + +# venv / buildout related bin/ -buildout-cache/ develop-eggs/ eggs/ -htmlcov/ +.eggs/ +etc/ +.installed.cfg include/ lib/ -local/ -node_modules/ -parts/ -dist/* -test.plone_addon/ -var/ -# files -.installed.cfg -.mr.developer.cfg lib64 -log.html -output.xml -pip-selfcheck.json -report.html -.vscode/ -.idea/ -.tox/ +.mr.developer.cfg +parts/ pyvenv.cfg -reports/ -# excludes +var/ + +# mxdev +/instance/ +/.make-sentinels/ +/*-mxdev.txt +/reports/ +/sources/ +/venv/ +.installed.txt + +requirements-mxdev.txt + +## +# Add extra configuration options in .meta.toml: +# [gitignore] +# extra_lines = """ +# _your own configuration lines_ +# """ +## diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 1190cc4..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,70 +0,0 @@ -# This file is a template, and might need editing before it works on your project. -# Official language image. Look for the different tagged releases at: -# https://hub.docker.com/r/library/plone/tags/ -image: python:2.7-stretch - -# Change pip's cache directory to be inside the project directory since we can -# only cache local items. -variables: - PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache" - -# Pip's cache doesn't store the python packages -# https://pip.pypa.io/en/stable/reference/pip_install/#caching -# -# If you want to also cache the installed packages, you have to install -# them in a virtualenv and cache it as well. -cache: - paths: - - .cache/pip - - venv/ - - downloads/ - - eggs/ - -# Set execution order: first run jobs on 'test' stage on parallel -# then run jobs on 'report' stage -stages: - - test - - report - -before_script: - - echo "deb http://ftp.de.debian.org/debian/ stretch main contrib non-free" >> /etc/apt/sources.list - - apt-get update - - apt-get install -y firefoxdriver - - apt-get install -y xvfb - - python -V # Print out python version for debugging - - pip install virtualenv - - export LC_CTYPE=en_US.UTF-8 - - export LC_ALL=en_US.UTF-8 - - export LANG=en_US.UTF-8 - - virtualenv --clear -p python2.7 venv - - source venv/bin/activate - - pip install -r requirements.txt - - buildout bootstrap - - bin/buildout -n -c buildout.cfg code-analysis:return-status-codes=True - -code-analysis: - stage: test - # still not available, see: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5004 - # success_with_warnings: True - script: - - bin/code-analysis - -robot: - stage: test - script: - - export DISPLAY=:99.0 - # - sh -e /etc/init.d/xvfb start - - xvfb-run bin/test --all - -coverage: - stage: report - script: - - bin/createcoverage - - bin/coverage html - - bin/coverage report - when: on_success - - coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+\%)/' - artifacts: - paths: - - htmlcov diff --git a/.meta.toml b/.meta.toml new file mode 100644 index 0000000..41a9f49 --- /dev/null +++ b/.meta.toml @@ -0,0 +1,58 @@ +# Generated from: +# https://github.com/plone/meta/tree/master/config/default +# See the inline comments on how to expand/tweak this configuration file +[meta] +template = "default" +commit-id = "8c30aa23" + +[pyproject] +codespell_skip = "*.min.js,*.pot,*.po,*.yaml,*.json" +codespell_ignores = "vew" +dependencies_ignores = "['plone.restapi', 'plone.volto', 'zestreleaser.towncrier', 'zest.releaser', 'pytest', 'pytest-cov', 'pytest-plone', 'pytest-docker', 'pytest-vcr', 'pytest-mock', 'gocept.pytestlayer', 'requests-mock', 'vcrpy']" +dependencies_mappings = [ + "Plone = ['Products.CMFPlone', 'Products.CMFCore', 'Products.GenericSetup', 'Products.PluggableAuthService']", + ] +check_manifest_ignores = """ + "news/*", + "constraints-mxdev.txt", + "requirements-mxdev.txt", +""" +towncrier_issue_format = "[#{issue}](https://github.com/collective/pas.plugins.oidc/issues/{issue})" +extra_lines = """ +[tool.coverage.run] +omit = ["*/locales/*"] + +[tool.bandit] +targets = "src" +exclude_dirs = ["tests", "src/pas/plugins/oidc/locales"] +""" + +[gitignore] +extra_lines = """ +requirements-mxdev.txt +""" + +[tox] +test_runner = "pytest" +test_path = "/tests" +use_mxdev = true + +[github] +ref = "1.0.0" +jobs = [ + "qa", + "test", + "coverage", + "dependencies", + "release_ready", + ] + +[pre_commit] +extra_lines = """ +- repo: https://github.com/PyCQA/bandit + rev: '1.7.5' + hooks: + - id: bandit + args: ["-c", "pyproject.toml"] + additional_dependencies: ["bandit[toml]"] +""" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..907d758 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,100 @@ +# Generated from: +# https://github.com/plone/meta/tree/master/config/default +# See the inline comments on how to expand/tweak this configuration file +ci: + autofix_prs: false + autoupdate_schedule: monthly + +repos: +- repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade + args: [--py38-plus] +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort +- repo: https://github.com/psf/black + rev: 23.10.1 + hooks: + - id: black +- repo: https://github.com/collective/zpretty + rev: 3.1.0 + hooks: + - id: zpretty + +## +# Add extra configuration options in .meta.toml: +# [pre_commit] +# zpretty_extra_lines = """ +# _your own configuration lines_ +# """ +## +- repo: https://github.com/PyCQA/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + +## +# Add extra configuration options in .meta.toml: +# [pre_commit] +# flake8_extra_lines = """ +# _your own configuration lines_ +# """ +## +- repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + additional_dependencies: + - tomli + +## +# Add extra configuration options in .meta.toml: +# [pre_commit] +# codespell_extra_lines = """ +# _your own configuration lines_ +# """ +## +- repo: https://github.com/mgedmin/check-manifest + rev: "0.49" + hooks: + - id: check-manifest +- repo: https://github.com/regebro/pyroma + rev: "4.2" + hooks: + - id: pyroma +- repo: https://github.com/mgedmin/check-python-versions + rev: "0.22.0" + hooks: + - id: check-python-versions + args: ['--only', 'setup.py,pyproject.toml'] +- repo: https://github.com/collective/i18ndude + rev: "6.1.0" + hooks: + - id: i18ndude + + +## +# Add extra configuration options in .meta.toml: +# [pre_commit] +# i18ndude_extra_lines = """ +# _your own configuration lines_ +# """ +## + +- repo: https://github.com/PyCQA/bandit + rev: '1.7.5' + hooks: + - id: bandit + args: ["-c", "pyproject.toml"] + additional_dependencies: ["bandit[toml]"] + +## +# Add extra configuration options in .meta.toml: +# [pre_commit] +# extra_lines = """ +# _your own configuration lines_ +# """ +## diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0bb2ac1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,61 +0,0 @@ -dist: bionic -language: python -cache: - pip: true - directories: - - eggs - - $HOME/buildout-cache - - $HOME/.buildout -python: - - "2.7" -matrix: - include: - - python: "2.7" - env: PLONE_VERSION=43 - - python: "2.7" - env: PLONE_VERSION=51 - - python: "2.7" - env: PLONE_VERSION=52 - - python: "3.7" - env: PLONE_VERSION=52 - fast_finish: true - -before_install: - - mkdir -p $HOME/buildout-cache/{downloads,eggs,extends} - - mkdir -p $HOME/.buildout - - echo "[buildout]" > $HOME/.buildout/default.cfg - - echo "download-cache = $HOME/buildout-cache/downloads" >> $HOME/.buildout/default.cfg - - echo "eggs-directory = $HOME/buildout-cache/eggs" >> $HOME/.buildout/default.cfg - - echo "extends-cache = $HOME/buildout-cache/extends" >> $HOME/.buildout/default.cfg - - echo "abi-tag-eggs = true" >> $HOME/.buildout/default.cfg - - git config --global user.email "travis@travis-ci.org" - - git config --global user.name "Travis CI" - - sudo apt-get install -y firefox-geckodriver - - virtualenv -p `which python` . - - bin/pip install -r requirements.txt -c constraints_plone$PLONE_VERSION.txt - - cp test_plone$PLONE_VERSION.cfg buildout.cfg - -install: - - travis_retry pip install -U tox coveralls coverage -c constraints.txt - -before_script: - - 'export DISPLAY=:99.0' - - export VERBOSE=true - - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & - - sleep 3 - -script: - - PYTEST_ADDOPTS="-s -vv" tox - -after_success: - - python -m coverage.pickle2json - - coverage combine - - coveralls - -notifications: - email: - recipients: -# - travis-reports@plone.com - - {author} - on_success: change - on_failure: change diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..a4b70fd --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,59 @@ +# Changelog + + + + + +## 1.0a6 (2023-07-20) + +- Added Spanish translation [@macagua] + +- Added improvements about i18n support [macagua] + +- Drop python 2.7 and Plone 4 support [@erral] + +- Add support for the post_logout parameter for logout api. [@ramiroluz] + + +## 1.0a5 (2023-04-05) + +- Catch exceptions during the OAuth process [@erral] + +- Update the plugin to make challenges. + An anonymous user who visits a page for which you have to be authenticated, + is redirected to the new require_login view on the plugin. + This works the same way as the standard require_login page of Plone. + [@maurits] + +- Add a property for the default userinfo instead of using only sub. [@eikichi18] + + +## 1.0a4 (2023-01-16) + +- Call getProperty only once when getting redirect_uris or scope. [@maurits] + +- use getProperty accessor [@mamico] + + +## 1.0a3 (2022-10-30) + +- Removed the hardcoded auth cookie name [@alecghica] + +- Fixed Python compatibility with version >= 3.6 [@alecghica] + +- check if url is in portal before redirect #2 [@erral] + +- manage came_from [@mamico] + +## 1.0a2 (unreleased) + +- do userinforequest if there is a client.userinfo_endpoint [@mamico] + +## 1.0a1 (unreleased) + +- Initial release. [@mamico] diff --git a/CHANGES.rst b/CHANGES.rst deleted file mode 100644 index a45e714..0000000 --- a/CHANGES.rst +++ /dev/null @@ -1,73 +0,0 @@ -Changelog -========= - - -1.0a7 (unreleased) ------------------- - -- Allow dict instances to hold userinfo - [erral] - -1.0a6 (2023-07-20) ------------------- - -- Added Spanish translation - [macagua] - -- Added improvements about i18n support - [macagua] - -- Drop python 2.7 and Plone 4 support - [erral] - -- Add support for the post_logout parameter for logout api. - [ramiroluz] - - -1.0a5 (2023-04-05) ------------------- - -- Catch exceptions during the OAuth process - [erral] -- Update the plugin to make challenges. - An anonymous user who visits a page for which you have to be authenticated, - is redirected to the new require_login view on the plugin. - This works the same way as the standard require_login page of Plone. - [maurits] -- Add a property for the default userinfo instead of using only sub. - [eikichi18] - - -1.0a4 (2023-01-16) ------------------- - -- Call getProperty only once when getting redirect_uris or scope. - [maurits] - -- use getProperty accessor - [mamico] - - -1.0a3 (2022-10-30) ------------------- - -- Removed the hardcoded auth cookie name - [alecghica] -- Fixed Python compatibility with version >= 3.6 - [alecghica] -- check if url is in portal before redirect #2 - [erral] -- manage came_from - [mamico] - -1.0a2 (unreleased) ------------------- - -- do userinforequest if there is a client.userinfo_endpoint - [mamico] - -1.0a1 (unreleased) ------------------- - -- Initial release. - [mamico] diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.md similarity index 76% rename from CONTRIBUTORS.rst rename to CONTRIBUTORS.md index 8f62925..a9ab2cd 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.md @@ -1,7 +1,7 @@ -Contributors -============ +# Contributors - mamico, mauro.amico@gmail.com - erral, Mikel Larreategi - alecghica, alec.ghica@eaudeweb.ro - macagua, leonardocaballero@gmail.com +- ericof, ericof@plone.org diff --git a/DEVELOP.md b/DEVELOP.md new file mode 100644 index 0000000..fe80117 --- /dev/null +++ b/DEVELOP.md @@ -0,0 +1,36 @@ +## Local Development + +You need a working `python` environment (system, `virtualenv`, `pyenv`, etc) version 3.8 or superior. + +Then install the dependencies and a development instance using: + +```bash +make build +``` +### Update translations + +```bash +make i18n +``` + +### Format codebase + +```bash +make format +``` + +### Run tests + +Testing of this package is done with [`pytest`](https://docs.pytest.org/) and [`tox`](https://tox.wiki/). + +Run all tests with: + +```bash +make test +``` + +Run all tests but stop on the first error and open a `pdb` session: + +```bash +./bin/tox -e test -- -x --pdb +``` diff --git a/DEVELOP.rst b/DEVELOP.rst deleted file mode 100644 index 1e81a15..0000000 --- a/DEVELOP.rst +++ /dev/null @@ -1,42 +0,0 @@ -Using the development buildout -============================== - -Create a virtualenv in the package:: - - $ virtualenv --clear . - -Install requirements with pip:: - - $ ./bin/pip install -r requirements.txt - -Run buildout:: - - $ ./bin/buildout - -Start Plone in foreground: - - $ ./bin/instance fg - - -Running tests -------------- - - $ tox - -list all tox environments: - - $ tox -l - plone52-py37 - plone52-py38 - plone52-py39 - plone60-py37 - plone60-py38 - plone60-py39 - plone60-py310 - plone60-py311 - - -run a specific tox env: - - $ tox -e plone52-py37 - diff --git a/LICENSE.rst b/LICENSE.md similarity index 100% rename from LICENSE.rst rename to LICENSE.md diff --git a/MANIFEST.in b/MANIFEST.in index 44e3531..3d2b8ed 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,13 @@ graft src/pas graft docs -include *.rst global-exclude *.pyc +include *.md +include *.txt +include *.yaml +include .coveragerc +include Makefile +recursive-include tests *.py +recursive-include tests *.gitkeep +recursive-include tests *.json +recursive-include tests *.yml +include tests/keycloak/Dockerfile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c719aa5 --- /dev/null +++ b/Makefile @@ -0,0 +1,144 @@ +### Defensive settings for make: +# https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +.SHELLFLAGS:=-xeu -o pipefail -O inherit_errexit -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules + +# We like colors +# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects +RED=`tput setaf 1` +GREEN=`tput setaf 2` +RESET=`tput sgr0` +YELLOW=`tput setaf 3` + +BACKEND_FOLDER=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) +DOCS_DIR=${BACKEND_FOLDER}/docs + +# Python checks +PYTHON?=python3 + +# installed? +ifeq (, $(shell which $(PYTHON) )) + $(error "PYTHON=$(PYTHON) not found in $(PATH)") +endif + +# version ok? +PYTHON_VERSION_MIN=3.8 +PYTHON_VERSION_OK=$(shell $(PYTHON) -c "import sys; print((int(sys.version_info[0]), int(sys.version_info[1])) >= tuple(map(int, '$(PYTHON_VERSION_MIN)'.split('.'))))") +ifeq ($(PYTHON_VERSION_OK),0) + $(error "Need python $(PYTHON_VERSION) >= $(PYTHON_VERSION_MIN)") +endif + +all: build + +# Add the following 'help' target to your Makefile +# And add help text after each target name starting with '\#\#' +.PHONY: help +help: ## This help message + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: clean +clean: clean-build clean-pyc clean-test clean-venv clean-instance ## remove all build, test, coverage and Python artifacts + +.PHONY: clean-instance +clean-instance: ## remove existing instance + rm -fr instance etc inituser var + +.PHONY: clean-venv +clean-venv: ## remove virtual environment + rm -fr bin include lib lib64 env pyvenv.cfg .tox .pytest_cache requirements-mxdev.txt + +.PHONY: clean-build +clean-build: ## remove build artifacts + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -rf {} + + +.PHONY: clean-pyc +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +.PHONY: clean-test +clean-test: ## remove test and coverage artifacts + rm -f .coverage + rm -fr htmlcov/ + +bin/pip bin/tox bin/mxdev: + @echo "$(GREEN)==> Setup Virtual Env$(RESET)" + $(PYTHON) -m venv . + bin/pip install -U "pip" "wheel" "cookiecutter" "mxdev" "tox" + +.PHONY: config +config: bin/pip ## Create instance configuration + @echo "$(GREEN)==> Create instance configuration$(RESET)" + bin/cookiecutter -f --no-input --config-file instance.yaml gh:plone/cookiecutter-zope-instance + +.PHONY: install-plone-6.0 +install-plone-6.0: config ## pip install Plone packages + @echo "$(GREEN)==> Setup Build$(RESET)" + bin/mxdev -c mx.ini + bin/pip install -r requirements-mxdev.txt + +.PHONY: install +install: install-plone-6.0 ## Install Plone 6.0 + +.PHONY: start +start: ## Start a Plone instance on localhost:8080 + PYTHONWARNINGS=ignore ./bin/runwsgi instance/etc/zope.ini + +.PHONY: console +console: ## Console + PYTHONWARNINGS=ignore ./bin/zconsole debug instance/etc/zope.ini + +.PHONY: format +format: bin/tox ## Format the codebase according to our standards + @echo "$(GREEN)==> Format codebase$(RESET)" + bin/tox -e format + +.PHONY: lint +lint: bin/tox ## check code style + bin/tox -e lint + +# i18n +bin/i18ndude bin/pocompile: bin/pip + @echo "$(GREEN)==> Install translation tools$(RESET)" + bin/pip install i18ndude zest.pocompile + +.PHONY: i18n +i18n: bin/i18ndude ## Update locales + @echo "$(GREEN)==> Updating locales$(RESET)" + bin/update_locale + bin/pocompile src/ + +# Tests +.PHONY: test +test: bin/tox ## run tests + bin/tox -e test + +.PHONY: test-coverage +test-coverage: bin/tox ## run tests + bin/tox -e coverage + +# Docs +bin/sphinx-build: bin/pip + bin/pip install -r requirements-docs.txt + +.PHONY: build-docs +build-docs: bin/sphinx-build ## Build the documentation + ./bin/sphinx-build \ + -b html $(DOCS_DIR) "$(DOCS_DIR)/_build/html" + +.PHONY: livehtml +livehtml: bin/sphinx-build ## Rebuild Sphinx documentation on changes, with live-reload in the browser + ./bin/sphinx-autobuild \ + --ignore "*.swp" \ + -b html $(DOCS_DIR) "$(DOCS_DIR)/_build/html" diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ccaa60 --- /dev/null +++ b/README.md @@ -0,0 +1,264 @@ +
logo
+ +

pas.plugins.oidc

+ +
+ +[![PyPI](https://img.shields.io/pypi/v/pas.plugins.oidc)](https://pypi.org/project/pas.plugins.oidc/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pas.plugins.oidc)](https://pypi.org/project/pas.plugins.oidc/) +[![PyPI - Wheel](https://img.shields.io/pypi/wheel/pas.plugins.oidc)](https://pypi.org/project/pas.plugins.oidc/) +[![PyPI - License](https://img.shields.io/pypi/l/pas.plugins.oidc)](https://pypi.org/project/pas.plugins.oidc/) +[![PyPI - Status](https://img.shields.io/pypi/status/pas.plugins.oidc)](https://pypi.org/project/pas.plugins.oidc/) + + +[![PyPI - Plone Versions](https://img.shields.io/pypi/frameworkversions/plone/pas.plugins.oidc)](https://pypi.org/project/pas.plugins.oidc/) + +[![Meta](https://github.com/collective/pas.plugins.oidc/actions/workflows/meta.yml/badge.svg)](https://github.com/collective/pas.plugins.oidc/actions/workflows/meta.yml) +![Code Style](https://img.shields.io/badge/Code%20Style-Black-000000) + +[![GitHub contributors](https://img.shields.io/github/contributors/collective/pas.plugins.oidc)](https://github.com/collective/pas.plugins.oidc) +[![GitHub Repo stars](https://img.shields.io/github/stars/collective/pas.plugins.oidc?style=social)](https://github.com/collective/pas.plugins.oidc) + +
+ +## Intro +This is a Plone authentication plugin for OpenID Connect. +OAuth 2.0 should work as well because OpenID Connect is built on top of this protocol. + +## Features + +- PAS plugin, although currently no interfaces are activated. +- Three browser views for this PAS plugin, which are the main interaction with the outside world. + + +## Installation + +This package supports Plone sites using Volto and ClassicUI. + +For proper Volto support, the requirements are: + +* plone.restapi >= 8.34.0 +* Volto >= 16.10.0 + +Add **pas.plugins.oidc** to the Plone installation using `pip`: + +``bash +pip install pas.plugins.oidc +`` + +### Warning + +Pay attention to the customization of `User info property used as userid` field, with the wrong configuration it's easy to impersonate another user. + + +## Configure the plugin + +* Go to the Add-ons control panel and install `pas.plugins.oidc`. +* In the ZMI go to the plugin properties at `http://localhost:8080/Plone/acl_users/oidc/manage_propertiesForm` +* Configure the properties with the data obtained from your provider: + * `OIDC/Oauth2 Issuer` + * `Client ID` + * `Client secret` + * `redirect_uris`: this needs to match the **public URL** where the user will be redirected after the login flow is completed. It needs to include + the `/Plone/acl_users/oidc/callback` part. When using Volto you need to expose Plone somehow to have the login process finish correctly. + * `Use Zope session data manager`: see the section below about the usage of session. + * `Create user / update user properties`: when selected the user data in Plone will be updated with the data coming from the OIDC provider. + * `Create authentication __ac ticket`: when selected the user will be allowed to act as a logged-in user in Plone. + * `Create authentication auth_token (Volto/REST API) ticket`: when selected the user will be allowed to act as a logged-in user in the Volto frontend. + * `Open ID scopes to request to the server`: information requested to the OIDC provider. Leave it as it is or modify it according to your provider's information. + * `Use PKCE`: when enabled uses [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) when requesting authentication from the provider. + +### Login and Logout URLs + +When using this plugin with *Plone 6 Classic UI* the standard URLs used for login (`http://localhost:8080/Plone/login`) and logout (`http://localhost:8080/Plone/logout`) +will not trigger the usage of the plugin. + +When using this plugin with a [Volto frontend](https://6.docs.plone.org/volto/index.html) the standard URLs for login (`http://localhost:3000/login`) +and logout (`http://localhost:3000/logout`) will not trigger the usage of the plugin. + +To login into a site using the OIDC provider, you will need to change those login URLs to the following: + +* **Login URL**: /``/acl_users/``/login +* **Logout URL**: /``/acl_users/``/logout + +*Where:* + + * `Plone Site Id`: is the id you gave to the Plone site when you created it. It is usually `Plone` but may vary. It is the last part of the URL when you browse Plone directly without using any proxy server, ex. `http://localhost:8080/Plone+` -> `Plone`. + + * `oidc pas plugin id`: is the id you gave to the OIDC plugin when you created it inside the Plone PAS administration panel. If you just used the default configuration and installed this plugin using Plone's Add-on Control Panel, this id will be `oidc`. + +When using Volto as a frontend, you need to expose those login and logout URLs somehow to make the login and logout process work. + + +### Example setup with Keycloak + +##### Setup Keycloak as server + +Please refer to the [Keycloak documentation](https://www.keycloak.org/documentation>) for up to date instructions. +Specifically, here we will use a Docker image, so follow the instructions on how to [get started with Keycloak on Docker](https://www.keycloak.org/getting-started/getting-started-docker). + +This does **not** give you a production setup, but it is fine for local development. + +**Note:** Keycloak runs on port `8080` by default. Plone uses the same port. When you are reading this, you probably know how to let Plone use a different port. +So let's indeed let Keycloak use its preferred port. At the moment of writing, this is how you start a Keycloak container: + +```shell +docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:19.0.3 start-dev +``` +The plugin can be used with legacy (deprecated) Keycloak `redirect_uri` parameter. To use this you need to enable the option +in the plugin configuration. To test that you can run the Keycloak server with the `--spi-login-protocol-openid-connect-legacy-logout-redirect-uri=true` +option: + +```shell +docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:19.0.3 start-dev --spi-login-protocol-openid-connect-legacy-logout-redirect-uri=true +``` + +**Note:** when you exit this container, it still exists and you can restart it so you don't lose your configuration. +With `docker ps -a` figure out the name of the container and then use `docker container start -ai `. + +Follow the Keycloak Docker documentation further: + +* Open the [Keycloak Admin Console](http://localhost:8080/admin), make sure you are logged in as `admin`. +* Click the word `master` in the top-left corner, then click `Create Realm`. +* Enter *plone* in the `Realm name` field. +* Click `Create`. +* Click the word `master` in the top-left corner, then click `plone`. +* Click `Manage` -> `Users` in the left-hand menu. +* Click `Create new user`. +* Remember to set a password for this user in the `Credentials` tab. +* Open a different browser and check that you can login to [Keycloak Account Console](http://localhost:8080/realms/plone/account) with this user. + +In the original browser, follow the steps for securing your first app. +But we will be using different settings for Plone. +And when last I checked, the actual UI differed from the documentation. + +So: +* Open the [Keycloak Admin Console](http://localhost:8080/admin), make sure you are logged in as `admin`. +* Click the word `master` in the top-left corner, then click `plone`. +* Click `Manage` -> `Clients` in the left-hand menu. +* Click `Create client`: + * `Client type`: *OpenID Connect* + * `Client ID`: *plone* + * Turn `Always display in console` to `On`, *Useful for testing*. + * Click `Next` and click `Save`. +* Now you can fill in the `Settings` -> `Access settings`. We will assume Plone runs on port `8081`: + * `Root URL`: `http://localhost:8081/Plone/` + * `Home URL`: `http://localhost:8081/Plone/` + * `Valid redirect URIs`: `http://localhost:8081/Plone*` + **Tip:** Leave the rest at the defaults, unless you know what you are doing. +* Now you can fill in the `Settings` -> `Capability config`. + * Turn `Client authentication` to `On`. This defines the type of the OIDC client. When it's ON, the + OIDC type is set to confidential access type. When it's OFF, it is set to public access type. + * Click `Save`. +* Now you can access `Credentials` -> `Client secret` and click on the clipboard icon to copy it. This will + be necessary to configure the plugin in Plone. + +**Keycloak is ready done configured!** + +#### Setup Plone as a client + +* In your Zope instance configuration, make sure Plone runs on port 8081. +* Make sure [pas.plugins.oidc` is installed with `pip `_ or `Buildout](https://www.buildout.org/). +* Start Plone and create a Plone site with id Plone. +* In the Add-ons control panel, install `pas.plugins.oidc`. +* In the ZMI go to the plugin properties at http://localhost:8081/Plone/acl_users/oidc/manage_propertiesForm +* Set these properties: + * `OIDC/Oauth2 Issuer`: http://localhost:8080/realms/plone/ + * `Client ID`: *plone* (**Warning:** This property must match the `Client ID` you have set in Keycloak.) + * `Client secret`: *••••••••••••••••••••••••••••••••* (**Warning:** This property must match the `Client secret` you have get in Keycloak.) + * `Use deprecated redirect_uri for logout url(/Plone/acl_users/oidc/logout)` checked. Use this if you need to run old versions of Keycloak. + * `Open ID scopes to request to the server`: this depends on which version of Keycloak you are using, and which scopes are available there. + In recent Keycloak versions, you *must* include `openid` as scope. + Suggestion is to use `openid` and `profile`. + * **Tip:** Leave the rest at the defaults, unless you know what you are doing. + * Click `Save`. + +**Plone is ready done configured!** + +See this screenshot: + +.. image:: docs/screenshot-settings.png + +**Warning:** + +Attention, before Keycloak 18, the parameter for logout was `redirect_uri` and it has been deprecated since version 18. But the +Keycloak server can run with the `redirect_uri` if needed, it is possible to use the plugin with the legacy `redirect_uri` +parameter enabled also. The problem is that if the deprecated parameter is enabled in the plugin but not in the server, the plugin +will not work. + +So, this is the way it works: +* With legacy `redirect_uri` parameter enabled in Keycloak, the plugin works in default mode. +* With legacy `redirect_uri` parameter enabled in Keycloak, the plugin also works with legacy mode. +* With legacy `redirect_uri` parameter disabled in Keycloak (default after version 18), the plugin works in default mode. +* With legacy `redirect_uri` parameter disabled in Keycloak (default after version 18), the plugin does NOT work with legacy mode. + +So, for Keycloak, it does not matter if we use the default or legacy mode if the Keycloak runs in legacy mode. + +*Notes:* + +* If legacy `redirect_uri` parameter is disabled in Keycloak, this is the default since version 18 of Keycloak according + to this comment in *Starck Overflow*: https://stackoverflow.com/a/72142887. +* The plugin will work only if the `Use deprecated redirect_uri for logout url(/Plone/acl_users/oidc/logout)` + option is un-checked at the plugin properties at http://localhost:8081/Plone/acl_users/oidc/manage_propertiesForm. + +#### Login + +Go to the other browser, or logout as admin from [Keycloak Admin Console](http://localhost:8080/admin). +Currently, the Plone login form is unchanged. + +Instead, for testing go to the login page of the plugin: http://localhost:8081/Plone/acl_users/oidc/login, +this will take you to Keycloak to login, and then return. You should now be logged in to Plone, and see the +*full name* and *email*, if you have set this in Keycloak. + +#### Logout + +If the login did work as expected you can try to Plone logout. +Currently, the Plone logout form is unchanged. + +Instead, for testing go to the logout page of the plugin: http://localhost:8081/Plone/acl_users/oidc/logout, +this will take you to Keycloak to logout, and then return to the post-logout redirect URL. + +## Usage of sessions in the login process + +This plugin uses sessions during the login process to identify the user while he goes to the OIDC provider +and comes back from there. + +The plugin has 2 ways of working with sessions: + +- Use the Zope Session Management: if the `Use Zope session data manager` option in the plugin configuration is enabled, + the plugin will use the sessioning configuration configured in Zope. To do so we advise using [Products.mcdutils](https://pypi.org/project/Products.mcdutils/) + to save the session data in a memcached based storage. Otherwise Zope will try to use ZODB based sessioning + which has shown several problems in the past. + +- Use the cookie-based session management: if the `Use Zope session data manager` option in the plugin + configuration is disabled, the plugin will use a Cookie to save that information in the client's browser. + +## Settings in environment variables + +Optionally, instead of editing your OIDC provider settings through the ZMI, you can use [collective.regenv](https://pypi.org/project/collective.regenv/) and provide +a `YAML` file with your settings. This is very useful if you have different settings in different environments +and you do not want to edit the settings each time you move the contents. + +## Varnish + +Optionally, if you are using the [Varnish caching server](https://6.docs.plone.org/glossary.html#term-Varnish) in front +of Plone, you may see this plugin only partially working. Especially the `came_from` parameter may be ignored. +This is because the buildout standard configuration from [plone.recipe.varnish](https://pypi.org/project/plone.recipe.varnish/) +removes most cookies to improve anonymous caching. + +The solution is to make sure the `__ac_session` cookie is added to the `cookie-pass` option. +Check what the current default is in the buildout recipe, and update it: + + +## Contribute + +- Issue Tracker: https://github.com/collective/pas.plugins.oidc/issues +- Source Code: https://github.com/collective/pas.plugins.oidc + +## References + +* Blog post: https://www.codesyntax.com/en/blog/log-in-in-plone-using-your-google-workspace-account + +## License + +The project is licensed under the GPLv2. diff --git a/README.rst b/README.rst deleted file mode 100644 index a127218..0000000 --- a/README.rst +++ /dev/null @@ -1,367 +0,0 @@ -.. This README is meant for consumption by humans and pypi. Pypi can render rst files so please do not use Sphinx features. - If you want to learn more about writing documentation, please check out: http://docs.plone.org/about/documentation_styleguide.html - This text does not appear on pypi or github. It is a comment. - -.. image:: https://img.shields.io/pypi/v/pas.plugins.oidc.svg - :target: https://pypi.python.org/pypi/pas.plugins.oidc/ - :alt: Latest Version - -.. image:: https://img.shields.io/pypi/status/pas.plugins.oidc.svg - :target: https://pypi.python.org/pypi/pas.plugins.oidc - :alt: Egg Status - -.. image:: https://img.shields.io/pypi/pyversions/pas.plugins.oidc.svg?style=plastic - :target: https://pypi.python.org/pypi/pas.plugins.oidc/ - :alt: Supported - Python Versions - -.. image:: https://img.shields.io/pypi/l/pas.plugins.oidc.svg - :target: https://pypi.python.org/pypi/pas.plugins.oidc/ - :alt: License - -.. image:: https://github.com/collective/pas.plugins.oidc/actions/workflows/tests.yml/badge.svg - :target: https://github.com/collective/pas.plugins.oidc/actions - :alt: Tests - -.. image:: https://coveralls.io/repos/github/collective/pas.plugins.oidc/badge.svg?branch=main - :target: https://coveralls.io/github/collective/pas.plugins.oidc?branch=main - :alt: Coverage - - -pas.plugins.oidc -================ - -This is a Plone authentication plugin for OpenID Connect. -OAuth 2.0 should work as well because OpenID Connect is built on top of this protocol. - -Features --------- - -- PAS plugin, although currently no interfaces are activated. -- Three browser views for this PAS plugin, which are the main interaction with the outside world. - - -Installation ------------- - -Install ``pas.plugins.oidc`` by adding it to your buildout: :: - - [buildout] - - ... - - eggs = - pas.plugins.oidc - - -and then running ``bin/buildout`` - -Warning -------- - -Pay attention to the customization of `User info property used as userid` field, with the wrong configuration it's easy to impersonate another user. - - -Install and configure the plugin --------------------------------- - -* Go to the Add-ons control panel and install ``pas.plugins.oidc``. - -* In the ZMI go to the plugin properties at http://localhost:8080/Plone/acl_users/oidc/manage_propertiesForm - -* Configure the properties with the data obtained from your provider: - - * ``OIDC/Oauth2 Issuer`` - - * ``Client ID`` - - * ``Client secret`` - - * ``redirect_uris``: this needs to match the **public URL** where the user will be redirected after the login flow is completed. It needs to include - the `/Plone/acl_users/oidc/callback` part. When using Volto you need to expose Plone somehow to have the login process finish correctly. - - * ``Use Zope session data manager``: see the section below about the usage of session. - - * ``Create user / update user properties``: when selected the user data in Plone will be updated with the data coming from the OIDC provider. - - * ``Create authentication __ac ticket``: when selected the user will be allowed to act as a logged-in user in Plone. - - * ``Create authentication auth_token (Volto/REST API) ticket``: when selected the user will be allowed to act as a logged-in user in the Volto frontend. - - * ``Open ID scopes to request to the server``: information requested to the OIDC provider. Leave it as it is or modify it according to your provider's information. - - * ``Use PKCE``: when enabled uses PKCE_ when requesting authentication from the provider. - ----- - -Login and Logout URLs ---------------------- - -When using this plugin with *Plone 6 Classic UI* the standard URLs used for login (`http://localhost:8080/Plone/login`) and logout (`http://localhost:8080/Plone/logout`) -will not trigger the usage of the plugin. - -When using this plugin with a `Volto frontend `_ the standard URLs for login (`http://localhost:3000/login`) -and logout (`http://localhost:3000/logout`) will not trigger the usage of the plugin. - -To login into a site using the OIDC provider, you will need to change those login URLs to the following: - -* **Login URL**: /````/acl_users/````/login - -* **Logout URL**: /````/acl_users/````/logout - - *Where:* - - * ``Plone Site Id``: is the id you gave to the Plone site when you created it. It is usually `Plone` but may vary. It is the last part of the URL when you browse Plone directly without using any proxy server, ex. `http://localhost:8080/Plone+` -> `Plone`. - - * ``oidc pas plugin id``: is the id you gave to the OIDC plugin when you created it inside the Plone PAS administration panel. If you just used the default configuration and installed this plugin using Plone's Add-on Control Panel, this id will be `oidc`. - -When using Volto as a frontend, you need to expose those login and logout URLs somehow to make the login and logout process work. - ----- - -Example setup with Keycloak ---------------------------- - -Setup Keycloak as server -~~~~~~~~~~~~~~~~~~~~~~~~ - -Please refer to the `Keycloak documentation `_ for up to date instructions. -Specifically, here we will use a Docker image, so follow the instructions on how to `get started with Keycloak on Docker `_. -This does **not** give you a production setup, but it is fine for local development. - -**Note:** Keycloak runs on port ``8080`` by default. Plone uses the same port. When you are reading this, you probably know how to let Plone use a different port. -So let's indeed let Keycloak use its preferred port. At the moment of writing, this is how you start a Keycloak container: :: - - docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:19.0.3 start-dev - -The plugin can be used with legacy (deprecated) Keycloak ``redirect_uri`` parameter. To use this you need to enable the option -in the plugin configuration. To test that you can run the Keycloak server with the ``--spi-login-protocol-openid-connect-legacy-logout-redirect-uri=true`` -option: :: - - docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:19.0.3 start-dev --spi-login-protocol-openid-connect-legacy-logout-redirect-uri=true - -**Note:** when you exit this container, it still exists and you can restart it so you don't lose your configuration. -With ``docker ps -a`` figure out the name of the container and then use ``docker container start -ai ``. - -Follow the Keycloak Docker documentation further: - -* Open the `Keycloak Admin Console `_, make sure you are logged in as ``admin``. - -* Click the word ``master`` in the top-left corner, then click ``Create Realm``. - -* Enter *plone* in the ``Realm name`` field. - -* Click ``Create``. - -* Click the word ``master`` in the top-left corner, then click ``plone``. - -* Click ``Manage`` -> ``Users`` in the left-hand menu. - -* Click ``Create new user``. - -* Remember to set a password for this user in the ``Credentials`` tab. - -* Open a different browser and check that you can login to `Keycloak Account Console `_ with this user. - -In the original browser, follow the steps for securing your first app. -But we will be using different settings for Plone. -And when last I checked, the actual UI differed from the documentation. -So: - -* Open the `Keycloak Admin Console `_, make sure you are logged in as ``admin``. - -* Click the word ``master`` in the top-left corner, then click ``plone``. - -* Click ``Manage`` -> ``Clients`` in the left-hand menu. - -* Click ``Create client``: - - * ``Client type``: *OpenID Connect* - - * ``Client ID``: *plone* - - * Turn ``Always display in console`` to ``On``, *Useful for testing*. - - * Click ``Next`` and click ``Save``. - -* Now you can fill in the ``Settings`` -> ``Access settings``. We will assume Plone runs on port ``8081``: - - * ``Root URL``: `http://localhost:8081/Plone/` - - * ``Home URL``: `http://localhost:8081/Plone/` - - * ``Valid redirect URIs``: `http://localhost:8081/Plone*` - - **Tip:** Leave the rest at the defaults, unless you know what you are doing. - -* Now you can fill in the ``Settings`` -> ``Capability config``. - - * Turn ``Client authentication`` to ``On``. This defines the type of the OIDC client. When it's ON, the - OIDC type is set to confidential access type. When it's OFF, it is set to public access type. - - * Click ``Save``. - -* Now you can access ``Credentials`` -> ``Client secret`` and click on the clipboard icon to copy it. This will - be necessary to configure the plugin in Plone. - -**Keycloak is ready done configured!** - ----- - -Setup Plone as a client -~~~~~~~~~~~~~~~~~~~~~~~ - -* In your Zope instance configuration, make sure Plone runs on port 8081. - -* Make sure ``pas.plugins.oidc`` is installed with `pip `_ or `Buildout `_. - -* Start Plone and create a Plone site with id Plone. - -* In the Add-ons control panel, install ``pas.plugins.oidc``. - -* In the ZMI go to the plugin properties at http://localhost:8081/Plone/acl_users/oidc/manage_propertiesForm - -* Set these properties: - - * ``OIDC/Oauth2 Issuer``: http://localhost:8080/realms/plone/ - - * ``Client ID``: *plone* - - **Warning:** This property must match the ``Client ID`` you have set in Keycloak. - - * ``Client secret``: *••••••••••••••••••••••••••••••••* - - **Warning:** This property must match the ``Client secret`` you have get in Keycloak. - - * ``Use deprecated redirect_uri for logout url(/Plone/acl_users/oidc/logout)`` checked. Use this if you need to run old versions of Keycloak. - - * ``Open ID scopes to request to the server``: this depends on which version of Keycloak you are using, and which scopes are available there. - In recent Keycloak versions, you *must* include ``openid`` as scope. - Suggestion is to use ``openid`` and ``profile``. - - * **Tip:** Leave the rest at the defaults, unless you know what you are doing. - - * Click ``Save``. - -**Plone is ready done configured!** - -See this screenshot: - -.. image:: docs/screenshot-settings.png - -*Warning:* - -Attention, before Keycloak 18, the parameter for logout was ``redirect_uri`` and it has been deprecated since version 18. But the -Keycloak server can run with the ``redirect_uri`` if needed, it is possible to use the plugin with the legacy ``redirect_uri`` -parameter enabled also. The problem is that if the deprecated parameter is enabled in the plugin but not in the server, the plugin -will not work. - -So, this is the way it works: - -* With legacy ``redirect_uri`` parameter enabled in Keycloak, the plugin works in default mode. - -* With legacy ``redirect_uri`` parameter enabled in Keycloak, the plugin also works with legacy mode. - -* With legacy ``redirect_uri`` parameter disabled in Keycloak (default after version 18), the plugin works in default mode. - -* With legacy ``redirect_uri`` parameter disabled in Keycloak (default after version 18), the plugin does NOT work with legacy mode. - -So, for Keycloak, it does not matter if we use the default or legacy mode if the Keycloak runs in legacy mode. - -*Notes:* - -* If legacy ``redirect_uri`` parameter is disabled in Keycloak, this is the default since version 18 of Keycloak according - to this comment in *Starck Overflow*: https://stackoverflow.com/a/72142887. - -* The plugin will work only if the ``Use deprecated redirect_uri for logout url(/Plone/acl_users/oidc/logout)`` - option is un-checked at the plugin properties at http://localhost:8081/Plone/acl_users/oidc/manage_propertiesForm. - ----- - -Login -~~~~~ - -Go to the other browser, or logout as admin from `Keycloak Admin Console `_. -Currently, the Plone login form is unchanged. - -Instead, for testing go to the login page of the plugin: http://localhost:8081/Plone/acl_users/oidc/login, -this will take you to Keycloak to login, and then return. You should now be logged in to Plone, and see the -*full name* and *email*, if you have set this in Keycloak. - -Logout -~~~~~~ - -If the login did work as expected you can try to Plone logout. -Currently, the Plone logout form is unchanged. - -Instead, for testing go to the logout page of the plugin: http://localhost:8081/Plone/acl_users/oidc/logout, -this will take you to Keycloak to logout, and then return to the post-logout redirect URL. - ----- - -Usage of sessions in the login process --------------------------------------- - -This plugin uses sessions during the login process to identify the user while he goes to the OIDC provider -and comes back from there. - -The plugin has 2 ways of working with sessions: - -- Use the Zope Session Management: if the ``Use Zope session data manager`` option in the plugin configuration is enabled, - the plugin will use the sessioning configuration configured in Zope. To do so we advise using `Products.mcdutils`_ - to save the session data in a memcached based storage. Otherwise Zope will try to use ZODB based sessioning - which has shown several problems in the past. - -- Use the cookie-based session management: if the ``Use Zope session data manager`` option in the plugin - configuration is disabled, the plugin will use a Cookie to save that information in the client's browser. - ----- - -Settings in environment variables ---------------------------------- - -Optionally, instead of editing your OIDC provider settings through the ZMI, you can use `collective.regenv`_ and provide -a ``YAML`` file with your settings. This is very useful if you have different settings in different environments -and you do not want to edit the settings each time you move the contents. - ----- - -Varnish -------- - -Optionally, if you are using the `Varnish caching server `_ in front -of Plone, you may see this plugin only partially working. Especially the ``came_from`` parameter may be ignored. -This is because the buildout standard configuration from `plone.recipe.varnish `_ -removes most cookies to improve anonymous caching. - -The solution is to make sure the ``__ac_session`` cookie is added to the ``cookie-pass`` option. -Check what the current default is in the buildout recipe, and update it: :: - - [varnish-configuration] - recipe = plone.recipe.varnish:configuration - ... - cookie-pass = "auth_token|__ac(|_(name|password|persistent|session))=":"\.(js|css|kss)$" - ----- - -Contribute ----------- - -- Issue Tracker: https://github.com/collective/pas.plugins.oidc/issues -- Source Code: https://github.com/collective/pas.plugins.oidc -- Documentation: https://docs.plone.org/foo/bar - - -References ----------- - -* Blog post: https://www.codesyntax.com/en/blog/log-in-in-plone-using-your-google-workspace-account - -License -------- - -The project is licensed under the GPLv2. - - -.. _`collective.regenv`: https://pypi.org/project/collective.regenv/ -.. _`Products.mcdutils`: https://pypi.org/project/Products.mcdutils/ -.. _PKCE: https://datatracker.ietf.org/doc/html/rfc7636 diff --git a/base.cfg b/base.cfg deleted file mode 100644 index 101d52b..0000000 --- a/base.cfg +++ /dev/null @@ -1,112 +0,0 @@ -[buildout] -show-picked-versions = true -extensions = - mr.developer - -parts = - instance - test -# we use tox for testing and linting, by default -# code-analysis - coverage - test-coverage - createcoverage - releaser - i18ndude - omelette - robot - plone-helper-scripts - vscode - -develop = . - - -[instance] -recipe = plone.recipe.zope2instance -user = admin:admin -http-address = 8080 -environment-vars = - zope_i18n_compile_mo_files true -eggs = - Plone - Pillow - pas.plugins.oidc [test] - -[vscode] -recipe = collective.recipe.vscode -eggs = ${instance:eggs} -autocomplete-use-omelette = True - -# [code-analysis] -# recipe = plone.recipe.codeanalysis -# directory = ${buildout:directory}/src/pas -# return-status-codes = False - - -[omelette] -recipe = collective.recipe.omelette -eggs = ${instance:eggs} - - -[test] -recipe = zc.recipe.testrunner -eggs = ${instance:eggs} -initialization = - os.environ['TZ'] = 'UTC' -defaults = ['-s', 'pas.plugins.oidc', '--auto-color', '--auto-progress'] - - -[coverage] -recipe = zc.recipe.egg -eggs = coverage - - -[test-coverage] -recipe = collective.recipe.template -input = inline: - #!/bin/bash - export TZ=UTC - ${buildout:directory}/bin/coverage run bin/test $* - ${buildout:directory}/bin/coverage html - ${buildout:directory}/bin/coverage report -m --fail-under=90 - # Fail (exit status 1) if coverage returns exit status 2 (this happens - # when test coverage is below 100%. -output = ${buildout:directory}/bin/test-coverage -mode = 755 - - -[createcoverage] -recipe = zc.recipe.egg -eggs = createcoverage - - -[robot] -recipe = zc.recipe.egg -eggs = - ${test:eggs} - plone.app.robotframework[debug,reload] - - -[releaser] -recipe = zc.recipe.egg -eggs = zest.releaser - - -[i18ndude] -recipe = zc.recipe.egg -eggs = i18ndude - -[plone-helper-scripts] -recipe = zc.recipe.egg -eggs = - Products.CMFPlone - ${instance:eggs} -interpreter = zopepy -scripts = - zopepy - plone-compile-resources - -[versions] -pygments = 2.14.0 -# Don't use a released version of pas.plugins.oidc -pas.plugins.oidc = diff --git a/bobtemplate.cfg b/bobtemplate.cfg deleted file mode 100644 index a591720..0000000 --- a/bobtemplate.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[main] -version = 5.2.3 -template = plone_addon -git_init = True -python = python3.7 diff --git a/buildout.cfg b/buildout.cfg deleted file mode 100644 index 8b296ec..0000000 --- a/buildout.cfg +++ /dev/null @@ -1,14 +0,0 @@ -[buildout] - -# use this extend one of the buildout configuration: -extends = -# -*- mrbob: extra extends -*- -# test_plone43.cfg -# test_plone50.cfg -# test_plone51.cfg -# test_plone52.cfg - test_plone60.cfg - -[instance] -eggs += - Products.PDBDebugMode diff --git a/constraints.txt b/constraints.txt index 24cbf87..228b6cc 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1 +1 @@ --c constraints_plone52.txt +-c https://dist.plone.org/release/6.0-latest/constraints.txt diff --git a/constraints_plone43.txt b/constraints_plone43.txt deleted file mode 100644 index f3a36b2..0000000 --- a/constraints_plone43.txt +++ /dev/null @@ -1,2 +0,0 @@ -setuptools==27.3.0 -zc.buildout==2.5.3 diff --git a/constraints_plone50.txt b/constraints_plone50.txt deleted file mode 100644 index 9852be6..0000000 --- a/constraints_plone50.txt +++ /dev/null @@ -1,3 +0,0 @@ --c https://dist.plone.org/release/5.0-latest/requirements.txt -# setuptools==33.1.1 -# zc.buildout==2.9.5 diff --git a/constraints_plone51.txt b/constraints_plone51.txt deleted file mode 100644 index 8dcbff1..0000000 --- a/constraints_plone51.txt +++ /dev/null @@ -1,3 +0,0 @@ --c https://dist.plone.org/release/5.1-latest/requirements.txt -# setuptools==39.1.0 -# zc.buildout==2.11.4 diff --git a/constraints_plone52.txt b/constraints_plone52.txt deleted file mode 100644 index 3dd8048..0000000 --- a/constraints_plone52.txt +++ /dev/null @@ -1,3 +0,0 @@ --c https://dist.plone.org/release/5.2-latest/requirements.txt -# setuptools==40.2.0 -# zc.buildout==2.13.2 diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index dcb2ed4..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,239 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [] - -# Add any paths that contain templates here, relative to this directory. -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' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'pas.plugins.oidc' -copyright = u'mamico (mamico)' -author = u'mamico (mamico)' - -# 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 = u'3.0' -# The full version, including alpha/beta/rc tags. -release = u'3.0' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# 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'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'alabaster' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. -# " v documentation" by default. -#html_title = u'bobtemplates.plone v3.0' - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (relative to this directory) to use as a favicon of -# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# 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'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not None, a 'Last updated on:' timestamp is inserted at every page -# bottom, using the given strftime format. -# The empty string is equivalent to '%b %d, %Y'. -#html_last_updated_fmt = None - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' -#html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# 'ja' uses this config value. -# 'zh' user can custom change `jieba` dictionary path. -#html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'pas.plugins.oidcdoc' - -# -- Options for LaTeX output --------------------------------------------- - -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', -} - - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index d4b9f96..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,5 +0,0 @@ -================ -pas.plugins.oidc -================ - -User documentation diff --git a/instance.yaml b/instance.yaml new file mode 100644 index 0000000..60834fc --- /dev/null +++ b/instance.yaml @@ -0,0 +1,8 @@ +default_context: + initial_user_name: 'admin' + initial_user_password: 'admin' + + load_zcml: + package_includes: ['pas.plugins.oidc'] + + db_storage: direct diff --git a/mx.ini b/mx.ini new file mode 100644 index 0000000..6e7929c --- /dev/null +++ b/mx.ini @@ -0,0 +1,15 @@ +; This is a mxdev configuration file +; it can be used to override versions of packages already defined in the +; constraints files and to add new packages from VCS like git. +; to learn more about mxdev visit https://pypi.org/project/mxdev/ + +[settings] +version-overrides = + plone.restapi>=8.40.0 + +; example section to use packages from git +; [example.contenttype] +; url = https://github.com/collective/example.contenttype.git +; pushurl = git@github.com:collective/example.contenttype.git +; extras = test +; branch = feature-7 diff --git a/news/.changelog_template.jinja b/news/.changelog_template.jinja new file mode 100644 index 0000000..b35bff3 --- /dev/null +++ b/news/.changelog_template.jinja @@ -0,0 +1,15 @@ +{% if sections[""] %} +{% for category, val in definitions.items() if category in sections[""] %} + +### {{ definitions[category]['name'] }} + +{% for text, values in sections[""][category].items() %} +- {{ text }} {{ values|join(', ') }} +{% endfor %} + +{% endfor %} +{% else %} +No significant changes. + + +{% endif %} \ No newline at end of file diff --git a/src/pas/plugins/oidc/tests/__init__.py b/news/.gitkeep similarity index 100% rename from src/pas/plugins/oidc/tests/__init__.py rename to news/.gitkeep diff --git a/news/32.feature b/news/32.feature new file mode 100644 index 0000000..addaa9a --- /dev/null +++ b/news/32.feature @@ -0,0 +1 @@ +Implement [plone/meta](https://github.com/plone/meta) and convert documentation to Markdown [@ericof] diff --git a/news/33.feature b/news/33.feature new file mode 100644 index 0000000..30e5ba0 --- /dev/null +++ b/news/33.feature @@ -0,0 +1 @@ +Drop support to Python 2.7 and Plone 5.2 [@ericof] diff --git a/news/34.internal b/news/34.internal new file mode 100644 index 0000000..f62e1f9 --- /dev/null +++ b/news/34.internal @@ -0,0 +1 @@ +Rewrite tests from unittest to pytest with [pytest-plone](https://pypi.org/project/pytest-plone/) [@ericof] diff --git a/news/35.internal b/news/35.internal new file mode 100644 index 0000000..cc0cc3a --- /dev/null +++ b/news/35.internal @@ -0,0 +1 @@ +Allow dict instances to hold userinfo [@erral] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..799155d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,173 @@ +# Generated from: +# https://github.com/plone/meta/tree/master/config/default +# See the inline comments on how to expand/tweak this configuration file +[build-system] +requires = ["setuptools>=68.2"] + +[tool.towncrier] +directory = "news/" +filename = "CHANGES.md" +start_string = "\n" +title_format = "## {version} ({project_date})" +template = "news/.changelog_template.jinja" +underlines = ["", "", ""] +issue_format = "[#{issue}](https://github.com/collective/pas.plugins.oidc/issues/{issue})" + +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking changes:" +showcontent = true + +[[tool.towncrier.type]] +directory = "feature" +name = "New features:" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bug fixes:" +showcontent = true + +[[tool.towncrier.type]] +directory = "internal" +name = "Internal:" +showcontent = true + +[[tool.towncrier.type]] +directory = "documentation" +name = "Documentation:" +showcontent = true + +[[tool.towncrier.type]] +directory = "tests" +name = "Tests" +showcontent = true + +## +# Add extra configuration options in .meta.toml: +# [pyproject] +# towncrier_extra_lines = """ +# extra_configuration +# """ +## + +[tool.isort] +profile = "plone" + +## +# Add extra configuration options in .meta.toml: +# [pyproject] +# isort_extra_lines = """ +# extra_configuration +# """ +## + +[tool.black] +target-version = ["py38"] + +## +# Add extra configuration options in .meta.toml: +# [pyproject] +# black_extra_lines = """ +# extra_configuration +# """ +## + +[tool.codespell] +ignore-words-list = "discreet,vew" +skip = "*.po,*.min.js,*.pot,*.po,*.yaml,*.json" +## +# Add extra configuration options in .meta.toml: +# [pyproject] +# codespell_ignores = "foo,bar" +# codespell_skip = "*.po,*.map,package-lock.json" +## + +[tool.dependencychecker] +Zope = [ + # Zope own provided namespaces + 'App', 'OFS', 'Products.Five', 'Products.OFSP', 'Products.PageTemplates', + 'Products.SiteAccess', 'Shared', 'Testing', 'ZPublisher', 'ZTUtils', + 'Zope2', 'webdav', 'zmi', + # ExtensionClass own provided namespaces + 'ExtensionClass', 'ComputedAttribute', 'MethodObject', + # Zope dependencies + 'AccessControl', 'Acquisition', 'AuthEncoding', 'beautifulsoup4', 'BTrees', + 'cffi', 'Chameleon', 'DateTime', 'DocumentTemplate', + 'MultiMapping', 'multipart', 'PasteDeploy', 'Persistence', 'persistent', + 'pycparser', 'python-gettext', 'pytz', 'RestrictedPython', 'roman', + 'soupsieve', 'transaction', 'waitress', 'WebOb', 'WebTest', 'WSGIProxy2', + 'z3c.pt', 'zc.lockfile', 'ZConfig', 'zExceptions', 'ZODB', 'zodbpickle', + 'zope.annotation', 'zope.browser', 'zope.browsermenu', 'zope.browserpage', + 'zope.browserresource', 'zope.cachedescriptors', 'zope.component', + 'zope.configuration', 'zope.container', 'zope.contentprovider', + 'zope.contenttype', 'zope.datetime', 'zope.deferredimport', + 'zope.deprecation', 'zope.dottedname', 'zope.event', 'zope.exceptions', + 'zope.filerepresentation', 'zope.globalrequest', 'zope.hookable', + 'zope.i18n', 'zope.i18nmessageid', 'zope.interface', 'zope.lifecycleevent', + 'zope.location', 'zope.pagetemplate', 'zope.processlifetime', 'zope.proxy', + 'zope.ptresource', 'zope.publisher', 'zope.schema', 'zope.security', + 'zope.sequencesort', 'zope.site', 'zope.size', 'zope.structuredtext', + 'zope.tal', 'zope.tales', 'zope.testbrowser', 'zope.testing', + 'zope.traversing', 'zope.viewlet' +] +'Products.CMFCore' = [ + 'docutils', 'five.localsitemanager', 'Missing', 'Products.BTreeFolder2', + 'Products.GenericSetup', 'Products.MailHost', 'Products.PythonScripts', + 'Products.StandardCacheManagers', 'Products.ZCatalog', 'Record', + 'zope.sendmail', 'Zope' +] +'plone.base' = [ + 'plone.batching', 'plone.registry', 'plone.schema','plone.z3cform', + 'Products.CMFCore', 'Products.CMFDynamicViewFTI', +] +python-dateutil = ['dateutil'] +ignore-packages = ['plone.restapi', 'plone.volto', 'zestreleaser.towncrier', 'zest.releaser', 'pytest', 'pytest-cov', 'pytest-plone', 'pytest-docker', 'pytest-vcr', 'pytest-mock', 'gocept.pytestlayer', 'requests-mock', 'vcrpy'] +Plone = ['Products.CMFPlone', 'Products.CMFCore', 'Products.GenericSetup', 'Products.PluggableAuthService'] + +## +# Add extra configuration options in .meta.toml: +# [pyproject] +# dependencies_ignores = "['zestreleaser.towncrier']" +# dependencies_mappings = [ +# "gitpython = ['git']", +# "pygithub = ['github']", +# ] +## + +[tool.check-manifest] +ignore = [ + ".editorconfig", + ".meta.toml", + ".pre-commit-config.yaml", + "tox.ini", + ".flake8", + "mx.ini", + "news/*", + "constraints-mxdev.txt", + "requirements-mxdev.txt", + +] +## +# Add extra configuration options in .meta.toml: +# [pyproject] +# check_manifest_ignores = """ +# "*.map.js", +# "*.pyc", +# """ +## + +[tool.coverage.run] +omit = ["*/locales/*"] + +[tool.bandit] +targets = "src" +exclude_dirs = ["tests", "src/pas/plugins/oidc/locales"] + +## +# Add extra configuration options in .meta.toml: +# [pyproject] +# extra_lines = """ +# _your own configuration lines_ +# """ +## diff --git a/requirements-6.0.x.txt b/requirements-6.0.x.txt deleted file mode 100644 index 3ae6c36..0000000 --- a/requirements-6.0.x.txt +++ /dev/null @@ -1 +0,0 @@ --r https://dist.plone.org/release/6.0-latest/requirements.txt diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000..1e26f94 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,10 @@ +Sphinx +jsx-lexer # volto +lesscpy +linkify-it-py +myst-parser +sphinx-autobuild +sphinx-book-theme +sphinx-copybutton +sphinx-togglebutton +sphinxcontrib-spelling diff --git a/requirements.txt b/requirements.txt index 06390bd..9dd6dd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -c constraints.txt -setuptools -zc.buildout +-e ".[test]" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 07e1dc0..0000000 --- a/setup.cfg +++ /dev/null @@ -1,39 +0,0 @@ -[check-manifest] -ignore = - *.cfg - *.txt - .coveragerc - .editorconfig - .gitattributes - .gitlab-ci.yml - tox.ini - -[isort] -# black compatible isort rules: -force_alphabetical_sort = True -force_single_line = True -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses = True -lines_after_imports = 2 -line_length = 88 -not_skip = - __init__.py -skip = - -[flake8] -# black compatible flake8 rules: -ignore = - W503, - C812, - E501 - T001 - C813 -# E203, E266 -exclude = bootstrap.py,docs,*.egg.,omelette -max-line-length = 88 -max-complexity = 18 -select = B,C,E,F,W,T4,B9 - -builtins = unicode,basestring diff --git a/setup.py b/setup.py index e0827ab..2dc6750 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,14 @@ -# -*- coding: utf-8 -*- """Installer for the pas.plugins.oidc package.""" - +from pathlib import Path from setuptools import find_packages from setuptools import setup -long_description = "\n\n".join( - [ - open("README.rst").read(), - open("CONTRIBUTORS.rst").read(), - open("CHANGES.rst").read(), - ] -) +long_description = f""" +{Path("README.md").read_text()}\n +{Path("CONTRIBUTORS.md").read_text()}\n +{Path("CHANGES.md").read_text()}\n +""" setup( @@ -19,16 +16,15 @@ version="1.0a7.dev0", description="An add-on for Plone", long_description=long_description, - long_description_content_type="text/x-rst", + long_description_content_type="text/markdown", # Get more from https://pypi.org/classifiers/ classifiers=[ + "Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: Plone", "Framework :: Plone :: Addon", - "Framework :: Plone :: 5.2", "Framework :: Plone :: 6.0", "Programming Language :: Python", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -52,25 +48,28 @@ package_dir={"": "src"}, include_package_data=True, zip_safe=False, - python_requires=">=3.7", + python_requires=">=3.8", install_requires=[ "setuptools", - # -*- Extra requirements: -*- - "z3c.jbot", - "plone.api>=1.8.4", - # 'oidcrp', - # "oic<1; python_version < 3", + "Plone", + "plone.api", + "plone.restapi>=8.34.0", "oic", ], extras_require={ "test": [ + "gocept.pytestlayer", "plone.app.testing", - # Plone KGS does not use this version, because it would break - # Remove if your package shall be part of coredev. - # plone_coredev tests as of 2016-04-01. - "plone.testing>=5.0.0", - "plone.app.contenttypes", - "plone.app.robotframework[debug]", + "plone.restapi[test]", + "pytest-cov", + "pytest-plone>=0.2.0", + "pytest-docker", + "pytest-mock", + "pytest", + "zest.releaser[recommended]", + "zestreleaser.towncrier", + "pytest-mock", + "requests-mock", ], }, entry_points=""" diff --git a/src/pas/__init__.py b/src/pas/__init__.py index 03d08ff..5284146 100644 --- a/src/pas/__init__.py +++ b/src/pas/__init__.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- __import__("pkg_resources").declare_namespace(__name__) diff --git a/src/pas/plugins/__init__.py b/src/pas/plugins/__init__.py index 03d08ff..5284146 100644 --- a/src/pas/plugins/__init__.py +++ b/src/pas/plugins/__init__.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- __import__("pkg_resources").declare_namespace(__name__) diff --git a/src/pas/plugins/oidc/__init__.py b/src/pas/plugins/oidc/__init__.py index 3f1698a..b5264aa 100644 --- a/src/pas/plugins/oidc/__init__.py +++ b/src/pas/plugins/oidc/__init__.py @@ -1,24 +1,26 @@ -# -*- coding: utf-8 -*- """Init and utils.""" from AccessControl.Permissions import manage_users as ManageUsers -from Products.PluggableAuthService.PluggableAuthService import ( # noqa - registerMultiPlugin, -) +from Products.PluggableAuthService import PluggableAuthService as PAS from zope.i18nmessageid import MessageFactory +import logging -_ = MessageFactory("pas.plugins.oidc") + +PACKAGE_NAME = "pas.plugins.oidc" + +_ = MessageFactory(PACKAGE_NAME) + +logger = logging.getLogger(PACKAGE_NAME) def initialize(context): # pragma: no cover """Initializer called when used as a Zope 2 product.""" from pas.plugins.oidc import plugins - registerMultiPlugin(plugins.OIDCPlugin.meta_type) + PAS.registerMultiPlugin(plugins.OIDCPlugin.meta_type) context.registerClass( plugins.OIDCPlugin, permission=ManageUsers, constructors=(plugins.add_oidc_plugin,), - # icon='www/PluggableAuthService.png', ) diff --git a/src/pas/plugins/oidc/browser/configure.zcml b/src/pas/plugins/oidc/browser/configure.zcml index 5f56254..ef60d99 100644 --- a/src/pas/plugins/oidc/browser/configure.zcml +++ b/src/pas/plugins/oidc/browser/configure.zcml @@ -2,38 +2,39 @@ xmlns="http://namespaces.zope.org/zope" xmlns:browser="http://namespaces.zope.org/browser" xmlns:plone="http://namespaces.plone.org/plone" - i18n_domain="pas.plugins.oidc"> + i18n_domain="pas.plugins.oidc" + > diff --git a/src/pas/plugins/oidc/browser/view.py b/src/pas/plugins/oidc/browser/view.py index 2cae341..0afdeec 100644 --- a/src/pas/plugins/oidc/browser/view.py +++ b/src/pas/plugins/oidc/browser/view.py @@ -5,30 +5,21 @@ from oic.oic.message import EndSessionRequest from oic.oic.message import IdToken from oic.oic.message import OpenIDSchema +from pas.plugins.oidc import _ +from pas.plugins.oidc import logger +from pas.plugins.oidc.plugins import OAuth2ConnectionException from pas.plugins.oidc.utils import SINGLE_OPTIONAL_BOOLEAN_AS_STRING from plone import api from Products.CMFCore.utils import getToolByName from Products.Five.browser import BrowserView +from urllib.parse import quote from zExceptions import Unauthorized -from pas.plugins.oidc.plugins import OAuth2ConnectionException -from pas.plugins.oidc import _ import base64 import json -import logging -try: - # Python 3 - from urllib.parse import quote -except ImportError: - # Python 2 - from urllib import quote - -logger = logging.getLogger(__name__) - - -class Session(object): +class Session: session_cookie_name = "__ac_session" _session = {} @@ -75,13 +66,14 @@ class RequireLoginView(BrowserView): def __call__(self): if api.user.is_anonymous(): # context is our PAS plugin - url = self.context.absolute_url() + "/login" - came_from = self.request.get('came_from', None) + base_url = self.context.absolute_url() + url = f"{base_url}/login" + came_from = self.request.get("came_from", None) if came_from: - url += "?came_from={}".format(quote(came_from)) + url = f"{url}?came_from={quote(came_from)}" else: url = api.portal.get().absolute_url() - url += "/insufficient-privileges" + url = f"{url}/insufficient-privileges" self.request.response.redirect(url) @@ -90,7 +82,9 @@ class LoginView(BrowserView): def __call__(self): session = Session( self.request, - use_session_data_manager=self.context.getProperty("use_session_data_manager"), + use_session_data_manager=self.context.getProperty( + "use_session_data_manager" + ), ) # state is used to keep track of responses to outstanding requests (state). # nonce is a string value used to associate a Client session with an ID Token, and to mitigate replay attacks. @@ -118,14 +112,11 @@ def __call__(self): "nonce": session.get("nonce"), "redirect_uri": self.context.get_redirect_uris(), } - if self.context.getProperty("use_pkce"): # Build a random string of 43 to 128 characters # and send it in the request as a base64-encoded urlsafe string of the sha256 hash of that string session.set("verifier", rndstr(128)) - args["code_challenge"] = self.get_code_challenge( - session.get("verifier") - ) + args["code_challenge"] = self.get_code_challenge(session.get("verifier")) args["code_challenge_method"] = "S256" try: @@ -134,10 +125,7 @@ def __call__(self): except Exception as e: logger.error(e) api.portal.show_message( - _( - "There was an error during the login process. Please try" - " again." - ) + _("There was an error during the login process. Please try" " again.") ) portal_url = api.portal.get_tool("portal_url") if came_from and portal_url.isURLInPortal(came_from): @@ -147,9 +135,7 @@ def __call__(self): return - self.request.response.setHeader( - "Cache-Control", "no-cache, must-revalidate" - ) + self.request.response.setHeader("Cache-Control", "no-cache, must-revalidate") self.request.response.redirect(login_url) return @@ -159,11 +145,7 @@ def get_code_challenge(self, value): See https://www.stefaanlippens.net/oauth-code-flow-pkce.html#PKCE-code-verifier-and-challenge """ hash_code = sha256(value.encode("utf-8")).digest() - return ( - base64.urlsafe_b64encode(hash_code) - .decode("utf-8") - .replace("=", "") - ) + return base64.urlsafe_b64encode(hash_code).decode("utf-8").replace("=", "") class LogoutView(BrowserView): @@ -184,18 +166,18 @@ def __call__(self): redirect_uri = api.portal.get().absolute_url() # Volto frontend mapping exception - if redirect_uri.endswith('/api'): + if redirect_uri.endswith("/api"): redirect_uri = redirect_uri[:-4] if self.context.getProperty("use_deprecated_redirect_uri_for_logout"): args = { "redirect_uri": redirect_uri, - } + } else: args = { "post_logout_redirect_uri": redirect_uri, "client_id": self.context.getProperty("client_id"), - } + } pas = getToolByName(self.context, "acl_users") auth_cookie_name = pas.credentials_cookie_auth.cookie_name @@ -216,15 +198,20 @@ def __call__(self): response = self.request.environ["QUERY_STRING"] session = Session( self.request, - use_session_data_manager=self.context.getProperty("use_session_data_manager"), + use_session_data_manager=self.context.getProperty( + "use_session_data_manager" + ), ) client = self.context.get_oauth2_client() aresp = client.parse_response( AuthorizationResponse, info=response, sformat="urlencoded" ) if aresp["state"] != session.get("state"): - logger.error("invalid OAuth2 state response:%s != session:%s", - aresp.get("state"), session.get("state")) + logger.error( + "invalid OAuth2 state response:%s != session:%s", + aresp.get("state"), + session.get("state"), + ) # TODO: need to double check before removing the comment below # raise ValueError("invalid OAuth2 state") @@ -281,7 +268,7 @@ def __call__(self): return else: logger.error( - "authentication failed invaid response %s %s", resp, userinfo + "authentication failed invalid response %s %s", resp, userinfo ) raise Unauthorized() else: @@ -298,7 +285,7 @@ def return_url(self, session=None): came_from = api.portal.get().absolute_url() # Volto frontend mapping exception - if came_from.endswith('/api'): + if came_from.endswith("/api"): came_from = came_from[:-4] return came_from diff --git a/src/pas/plugins/oidc/configure.zcml b/src/pas/plugins/oidc/configure.zcml index 542c0ad..76751ba 100644 --- a/src/pas/plugins/oidc/configure.zcml +++ b/src/pas/plugins/oidc/configure.zcml @@ -1,12 +1,16 @@ + i18n_domain="pas.plugins.oidc" + > - + @@ -21,26 +25,26 @@ diff --git a/src/pas/plugins/oidc/interfaces.py b/src/pas/plugins/oidc/interfaces.py index 6f68fc7..eb28036 100644 --- a/src/pas/plugins/oidc/interfaces.py +++ b/src/pas/plugins/oidc/interfaces.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- """Module where all interfaces, events and exceptions live.""" - from zope.publisher.interfaces.browser import IDefaultBrowserLayer diff --git a/src/pas/plugins/oidc/locales/README.rst b/src/pas/plugins/oidc/locales/README.rst deleted file mode 100644 index 8b7f9a3..0000000 --- a/src/pas/plugins/oidc/locales/README.rst +++ /dev/null @@ -1,37 +0,0 @@ -Adding and updating locales ---------------------------- - -For every language you want to translate into you need a -locales/[language]/LC_MESSAGES/collective.task.po -(e.g. locales/de/LC_MESSAGES/collective.task.po) - -For German - -.. code-block:: console - - $ mkdir de - -For updating locales - -.. code-block:: console - - $ ./update.sh - -Note ----- - -The script uses gettext package for internationalization. - -Install it before running the script. - -On macOS --------- - -.. code-block:: console - - $ brew install gettext - -On Windows ----------- - -see https://mlocati.github.io/articles/gettext-iconv-windows.html diff --git a/src/pas/plugins/oidc/locales/en/LC_MESSAGES/pas.plugins.oidc.po b/src/pas/plugins/oidc/locales/en/LC_MESSAGES/pas.plugins.oidc.po index 5ac09e9..8399fee 100644 --- a/src/pas/plugins/oidc/locales/en/LC_MESSAGES/pas.plugins.oidc.po +++ b/src/pas/plugins/oidc/locales/en/LC_MESSAGES/pas.plugins.oidc.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2023-05-29 12:03+0000\n" +"POT-Creation-Date: 2023-11-08 15:55+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -14,22 +14,22 @@ msgstr "" "Preferred-Encodings: utf-8 latin1\n" "Domain: DOMAIN\n" -#: ../configure.zcml:28 +#: pas/plugins/oidc/configure.zcml:32 msgid "Installs the pas.plugins.oidc add-on." msgstr "" -#: ../browser/view.py:140 +#: pas/plugins/oidc/browser/view.py:129 msgid "There was an error during the login process. Please try again." msgstr "" -#: ../configure.zcml:37 +#: pas/plugins/oidc/configure.zcml:41 msgid "Uninstalls the pas.plugins.oidc add-on." msgstr "" -#: ../configure.zcml:28 +#: pas/plugins/oidc/configure.zcml:32 msgid "pas.plugins.oidc" msgstr "" -#: ../configure.zcml:37 +#: pas/plugins/oidc/configure.zcml:41 msgid "pas.plugins.oidc (uninstall)" msgstr "" diff --git a/src/pas/plugins/oidc/locales/es/LC_MESSAGES/pas.plugins.oidc.po b/src/pas/plugins/oidc/locales/es/LC_MESSAGES/pas.plugins.oidc.po index ac4abfb..22b919a 100644 --- a/src/pas/plugins/oidc/locales/es/LC_MESSAGES/pas.plugins.oidc.po +++ b/src/pas/plugins/oidc/locales/es/LC_MESSAGES/pas.plugins.oidc.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: pas.plugins.oidc\n" -"POT-Creation-Date: 2023-05-29 12:03+0000\n" +"POT-Creation-Date: 2023-11-08 15:55+0000\n" "PO-Revision-Date: 2023-05-29 05:41-0400\n" "Last-Translator: Leonardo J. Caballero G. \n" "Language-Team: ES \n" @@ -18,22 +18,22 @@ msgstr "" "Language: es\n" "X-Is-Fallback-For: es-ar es-bo es-cl es-co es-cr es-do es-ec es-es es-sv es-gt es-hn es-mx es-ni es-pa es-py es-pe es-pr es-us es-uy es-ve\n" -#: ../configure.zcml:28 +#: pas/plugins/oidc/configure.zcml:32 msgid "Installs the pas.plugins.oidc add-on." msgstr "Instala el complemento pas.plugins.oidc." -#: ../browser/view.py:140 +#: pas/plugins/oidc/browser/view.py:129 msgid "There was an error during the login process. Please try again." msgstr "Hubo un error durante el proceso de inicio de sesión. Inténtalo de nuevo." -#: ../configure.zcml:37 +#: pas/plugins/oidc/configure.zcml:41 msgid "Uninstalls the pas.plugins.oidc add-on." msgstr "Desinstala el complemento pas.plugins.oidc." -#: ../configure.zcml:28 +#: pas/plugins/oidc/configure.zcml:32 msgid "pas.plugins.oidc" msgstr "pas.plugins.oidc" -#: ../configure.zcml:37 +#: pas/plugins/oidc/configure.zcml:41 msgid "pas.plugins.oidc (uninstall)" msgstr "pas.plugins.oidc (desinstalar)" diff --git a/src/pas/plugins/oidc/locales/pas.plugins.oidc.pot b/src/pas/plugins/oidc/locales/pas.plugins.oidc.pot index 2201384..fcef956 100644 --- a/src/pas/plugins/oidc/locales/pas.plugins.oidc.pot +++ b/src/pas/plugins/oidc/locales/pas.plugins.oidc.pot @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2023-05-29 12:03+0000\n" +"POT-Creation-Date: 2023-11-08 15:55+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,22 +17,22 @@ msgstr "" "Preferred-Encodings: utf-8 latin1\n" "Domain: pas.plugins.oidc\n" -#: ../configure.zcml:28 +#: pas/plugins/oidc/configure.zcml:32 msgid "Installs the pas.plugins.oidc add-on." msgstr "" -#: ../browser/view.py:140 +#: pas/plugins/oidc/browser/view.py:129 msgid "There was an error during the login process. Please try again." msgstr "" -#: ../configure.zcml:37 +#: pas/plugins/oidc/configure.zcml:41 msgid "Uninstalls the pas.plugins.oidc add-on." msgstr "" -#: ../configure.zcml:28 +#: pas/plugins/oidc/configure.zcml:32 msgid "pas.plugins.oidc" msgstr "" -#: ../configure.zcml:37 +#: pas/plugins/oidc/configure.zcml:41 msgid "pas.plugins.oidc (uninstall)" msgstr "" diff --git a/src/pas/plugins/oidc/locales/update.py b/src/pas/plugins/oidc/locales/update.py index d96402a..2225b49 100644 --- a/src/pas/plugins/oidc/locales/update.py +++ b/src/pas/plugins/oidc/locales/update.py @@ -1,74 +1,69 @@ -# -*- coding: utf-8 -*- +"""Update locales.""" +from pathlib import Path -import os -import pkg_resources -import subprocess # nosec B404 +import logging +import re +import subprocess -domain = "pas.plugins.oidc" -os.chdir(pkg_resources.resource_filename(domain, "")) -os.chdir("../../../") -target_path = "src/pas/oidc/" -locale_path = target_path + "locales/" -i18ndude = "./bin/i18ndude" +logger = logging.getLogger("i18n") +logger.setLevel(logging.DEBUG) + + +PATTERN = r"^[a-z]{2}.*" +domains = ("pas.plugins.oidc",) +cwd = Path.cwd() +target_path = Path(__file__).parent.parent.resolve() +locale_path = target_path / "locales" + +i18ndude = cwd / "bin" / "i18ndude" +if not i18ndude.exists(): + i18ndude = cwd / "i18ndude" # ignore node_modules files resulting in errors excludes = '"*.html *json-schema*.xml"' -def locale_folder_setup(): - os.chdir(locale_path) - languages = [d for d in os.listdir(".") if os.path.isdir(d)] - for lang in languages: - folder = os.listdir(lang) - if "LC_MESSAGES" in folder: +def locale_folder_setup(domain: str): + languages = [path for path in locale_path.glob("*") if path.is_dir()] + for lang_folder in languages: + lc_messages_path = lang_folder / "LC_MESSAGES" + lang = lang_folder.name + if lc_messages_path.exists(): continue - else: - lc_messages_path = lang + "/LC_MESSAGES/" - os.mkdir(lc_messages_path) - cmd = "msginit --locale={0} --input={1}.pot --output={2}/LC_MESSAGES/{3}.po".format( # NOQA: E501 - lang, - domain, - lang, - domain, - ) - subprocess.call( - cmd, - shell=True, # nosec + elif re.match(PATTERN, lang): + lc_messages_path.mkdir() + cmd = ( + f"msginit --locale={lang} " + f"--input={locale_path}/{domain}.pot " + f"--output={locale_path}/{lang}/LC_MESSAGES/{domain}.po" ) + subprocess.call(cmd, shell=True) # nosec B404 - os.chdir("../../../../") - -def _rebuild(): - cmd = "{i18ndude} rebuild-pot --pot {locale_path}/{domain}.pot --exclude {excludes} --create {domain} {target_path}".format( # NOQA: E501 - i18ndude=i18ndude, - locale_path=locale_path, - domain=domain, - target_path=target_path, - excludes=excludes, - ) - subprocess.call( - cmd, - shell=True, # nosec +def _rebuild(domain: str): + cmd = ( + f"{i18ndude} rebuild-pot --pot {locale_path}/{domain}.pot " + f"--exclude {excludes} " + f"--create {domain} {target_path}" ) + subprocess.call(cmd, shell=True) # nosec B404 -def _sync(): - cmd = "{0} sync --pot {1}/{2}.pot {3}*/LC_MESSAGES/{4}.po".format( - i18ndude, - locale_path, - domain, - locale_path, - domain, - ) - subprocess.call( - cmd, - shell=True, # nosec +def _sync(domain: str): + cmd = ( + f"{i18ndude} sync --pot {locale_path}/{domain}.pot " + f"{locale_path}/*/LC_MESSAGES/{domain}.po" ) + subprocess.call(cmd, shell=True) # nosec B404 def update_locale(): - locale_folder_setup() - _sync() - _rebuild() + if i18ndude.exists(): + for domain in domains: + logger.info(f"Updating translations for {domain}") + locale_folder_setup(domain) + _rebuild(domain) + _sync(domain) + else: + logger.error("Not able to find i18ndude") diff --git a/src/pas/plugins/oidc/locales/update.sh b/src/pas/plugins/oidc/locales/update.sh deleted file mode 100755 index 5ad9954..0000000 --- a/src/pas/plugins/oidc/locales/update.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# i18ndude should be available in current $PATH (eg by running -# ``export PATH=$PATH:$BUILDOUT_DIR/bin`` when i18ndude is located in your buildout's bin directory) -# -# For every language you want to translate into you need a -# locales/[language]/LC_MESSAGES/pas.plugins.oidc.po -# (e.g. locales/de/LC_MESSAGES/pas.plugins.oidc.po) - -domain=pas.plugins.oidc - -i18ndude rebuild-pot --pot $domain.pot --create $domain ../ -i18ndude sync --pot $domain.pot */LC_MESSAGES/$domain.po diff --git a/src/pas/plugins/oidc/plugins.py b/src/pas/plugins/oidc/plugins.py index 2221422..5af1236 100644 --- a/src/pas/plugins/oidc/plugins.py +++ b/src/pas/plugins/oidc/plugins.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from AccessControl import ClassSecurityInfo from AccessControl.class_init import InitializeClass from contextlib import contextmanager @@ -6,46 +5,26 @@ from oic.oic.message import OpenIDSchema from oic.oic.message import RegistrationResponse from oic.utils.authn.client import CLIENT_AUTHN_METHOD +from pas.plugins.oidc import logger +from plone.base.utils import safe_text from plone.protect.utils import safeWrite from Products.CMFCore.utils import getToolByName - -from Products.PluggableAuthService.interfaces.plugins import IChallengePlugin -# from Products.PluggableAuthService.interfaces.plugins import IExtractionPlugin -# from Products.PluggableAuthService.interfaces.plugins import IPropertiesPlugin -# from Products.PluggableAuthService.interfaces.plugins import IRolesPlugin from Products.PluggableAuthService.interfaces.plugins import IAuthenticationPlugin +from Products.PluggableAuthService.interfaces.plugins import IChallengePlugin from Products.PluggableAuthService.interfaces.plugins import IUserAdderPlugin from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin from Products.PluggableAuthService.utils import classImplements +from secrets import choice from ZODB.POSException import ConflictError from zope.interface import implementer from zope.interface import Interface import itertools -import logging -import string import plone.api as api - -try: - # Plone 6.0+ - from plone.base.utils import safe_text -except ImportError: - # Plone 5.2 - from Products.CMFPlone.utils import safe_unicode as safe_text - -try: - # Python 3.6+ - from secrets import choice -except ImportError: - # Less secure. - # https://bandit.readthedocs.io/en/1.7.4/blacklists/blacklist_calls.html#b311-random - from random import choice +import string -logger = logging.getLogger(__name__) -# _MARKER = object() PWCHARS = string.ascii_letters + string.digits + string.punctuation -# LAST_UPDATE_USER_PROPERTY_KEY = 'last_autousermaker_update' class OAuth2ConnectionException(Exception): @@ -127,7 +106,12 @@ class OIDCPlugin(BasePlugin): label="Open ID scopes to request to the server", ), dict(id="use_pkce", type="boolean", mode="w", label="Use PKCE. "), - dict(id="use_deprecated_redirect_uri_for_logout", type="boolean", mode="w", label="Use deprecated redirect_uri for logout url(/Plone/acl_users/oidc/logout)."), + dict( + id="use_deprecated_redirect_uri_for_logout", + type="boolean", + mode="w", + label="Use deprecated redirect_uri for logout url(/Plone/acl_users/oidc/logout).", + ), dict( id="use_modified_openid_schema", type="boolean", @@ -138,13 +122,15 @@ class OIDCPlugin(BasePlugin): id="user_property_as_userid", type="string", mode="w", - label="User info property used as userid, default 'sub'" - ) + label="User info property used as userid, default 'sub'", + ), ) def rememberIdentity(self, userinfo): if not isinstance(userinfo, (OpenIDSchema, dict)): - raise AssertionError("userinfo should be an OpenIDSchema but is {}".format(type(userinfo))) + raise AssertionError( + f"userinfo should be an OpenIDSchema but is {type(userinfo)}" + ) # sub: machine-readable identifier of the user at this server; # this value is guaranteed to be unique per user, stable over time, # and never re-used @@ -214,9 +200,7 @@ def rememberIdentity(self, userinfo): group = api.group.get(gid) is_managed = group.getProperty("type") == oidc.upper() if is_managed and gid not in groupid: - api.group.remove_user( - group=group, username=user_id - ) + api.group.remove_user(group=group, username=user_id) # Add group memberships for gid in groupid: if gid not in groups: @@ -225,9 +209,7 @@ def rememberIdentity(self, userinfo): ) # Tag managed groups with "type" of plugin id if not group.getTool().hasProperty("type"): - group.getTool()._setProperty( - "type", "", "string" - ) + group.getTool()._setProperty("type", "", "string") group.setGroupProperties({"type": oidc.upper()}) api.group.add_user(group=group, username=user_id) @@ -245,16 +227,16 @@ def _updateUserProperties(self, user, userinfo): # TODO: mettere in config il mapping tra metadati che arrivano da oidc e properties su plone # TODO: warning nel caso non vengono tornati dati dell'utente userProps = {} - if "email" in userinfo: - userProps["email"] = userinfo["email"] - if "given_name" in userinfo and "family_name" in userinfo: - userProps["fullname"] = "{} {}".format( - userinfo["given_name"], userinfo["family_name"] - ) - elif "name" in userinfo and "family_name" in userinfo: - userProps["fullname"] = "{} {}".format( - userinfo["name"], userinfo["family_name"] - ) + email = userinfo.get("email", "") + name = userinfo.get("name", "") + given_name = userinfo.get("given_name", "") + family_name = userinfo.get("family_name", "") + if email: + userProps["email"] = email + if given_name and family_name: + userProps["fullname"] = f"{given_name} {family_name}" + elif name and family_name: + userProps["fullname"] = f"{name} {family_name}" # userProps[LAST_UPDATE_USER_PROPERTY_KEY] = time.time() if userProps: user.setProperties(**userProps) @@ -280,7 +262,7 @@ def _setupTicket(self, user_id): request = self.REQUEST response = request["RESPONSE"] pas.session._setupSession(user_id, response) - logger.debug("Done setting up session/ticket for %s" % user_id) + logger.debug(f"Done setting up session/ticket for {user_id}") def _setupJWTTicket(self, user_id, user): """Set up JWT authentication ticket (auth_token cookie). @@ -311,9 +293,7 @@ def get_oauth2_client(self): # 'error_description': "Policy 'Trusted Hosts' rejected request to client-registration service. Details: Host not trusted."} # use WebFinger - provider_info = client.provider_config( - self.getProperty("issuer") - ) # noqa + provider_info = client.provider_config(self.getProperty("issuer")) # noqa info = { "client_id": self.getProperty("client_id"), "client_secret": self.getProperty("client_secret"), @@ -321,11 +301,12 @@ def get_oauth2_client(self): client_reg = RegistrationResponse(**info) client.store_registration_info(client_reg) return client - except Exception as e: + except Exception as exc: # There may happen several connection errors in this process # we catch them here and raise a generic own exception to be able # to catch it wherever it happens without knowing the internals # of the OAuth2 process + logger.exception("Error getting OAuth2 client", exc_info=exc) raise OAuth2ConnectionException def get_redirect_uris(self): @@ -333,7 +314,7 @@ def get_redirect_uris(self): if redirect_uris: return [safe_text(uri) for uri in redirect_uris if uri] return [ - "{}/callback".format(self.absolute_url()), + f"{self.absolute_url()}/callback", ] def get_scopes(self): @@ -357,7 +338,7 @@ def challenge(self, request, response): """ # Go to the login view of the PAS plugin. logger.info("Challenge. Came from %s", request.URL) - url = "{}/require_login?came_from={}".format(self.absolute_url(), request.URL) + url = f"{self.absolute_url()}/require_login?came_from={request.URL}" response.redirect(url, lock=1) return True diff --git a/src/pas/plugins/oidc/profiles/default/browserlayer.xml b/src/pas/plugins/oidc/profiles/default/browserlayer.xml index 59ac4eb..a47803a 100644 --- a/src/pas/plugins/oidc/profiles/default/browserlayer.xml +++ b/src/pas/plugins/oidc/profiles/default/browserlayer.xml @@ -1,7 +1,6 @@ - + - + diff --git a/src/pas/plugins/oidc/profiles/default/metadata.xml b/src/pas/plugins/oidc/profiles/default/metadata.xml index 811662b..9820946 100644 --- a/src/pas/plugins/oidc/profiles/default/metadata.xml +++ b/src/pas/plugins/oidc/profiles/default/metadata.xml @@ -1,4 +1,4 @@ - + 1001 diff --git a/src/pas/plugins/oidc/profiles/uninstall/browserlayer.xml b/src/pas/plugins/oidc/profiles/uninstall/browserlayer.xml index b9c89f6..909e260 100644 --- a/src/pas/plugins/oidc/profiles/uninstall/browserlayer.xml +++ b/src/pas/plugins/oidc/profiles/uninstall/browserlayer.xml @@ -1,7 +1,6 @@ - + - + diff --git a/src/pas/plugins/oidc/setuphandlers.py b/src/pas/plugins/oidc/setuphandlers.py index c28b885..2cf91a2 100644 --- a/src/pas/plugins/oidc/setuphandlers.py +++ b/src/pas/plugins/oidc/setuphandlers.py @@ -1,18 +1,13 @@ -# -*- coding: utf-8 -*- +from pas.plugins.oidc import logger from pas.plugins.oidc.plugins import OIDCPlugin from pas.plugins.oidc.utils import PLUGIN_ID -from Products.CMFCore.utils import getToolByName +from plone import api from Products.CMFPlone.interfaces import INonInstallable from zope.interface import implementer -import logging - - -logger = logging.getLogger(__name__) - @implementer(INonInstallable) -class HiddenProfiles(object): +class HiddenProfiles: def getNonInstallableProfiles(self): """Hide uninstall profile from site-creation and quickinstaller.""" return [ @@ -23,7 +18,7 @@ def getNonInstallableProfiles(self): def post_install(context): """Post install script""" # Setup our request oidc plugin. - pas = getToolByName(context, "acl_users") + pas = api.portal.get_tool("acl_users") # Create plugin if it does not exist. if PLUGIN_ID not in pas.objectIds(): @@ -35,9 +30,7 @@ def post_install(context): logger.info("Created %s in acl_users.", PLUGIN_ID) plugin = getattr(pas, PLUGIN_ID) if not isinstance(plugin, OIDCPlugin): - raise ValueError( - "Existing PAS plugin {0} is not a OIDCPlugin.".format(PLUGIN_ID) - ) + raise ValueError(f"Existing PAS plugin {PLUGIN_ID} is not a OIDCPlugin.") # Activate all supported interfaces for this plugin. activate = [] @@ -47,9 +40,7 @@ def post_install(context): interface_name = info["id"] if plugin.testImplements(interface): activate.append(interface_name) - logger.info( - "Activating interface %s for plugin %s", interface_name, PLUGIN_ID - ) + logger.info(f"Activating interface {interface_name} for plugin {PLUGIN_ID}") plugin.manage_activateInterfaces(activate) logger.info("Plugins activated.") @@ -62,23 +53,19 @@ def post_install(context): if interface_name in ("IChallengePlugin",): iface = plugins._getInterfaceFromName(interface_name) plugins.movePluginsTop(iface, [PLUGIN_ID]) - logger.info("Moved %s to top of %s.", PLUGIN_ID, interface_name) + logger.info(f"Moved {PLUGIN_ID} to top of {interface_name}.") return plugin def activate_plugin(context, interface_name, move_to_top=False): - pas = getToolByName(context, "acl_users") + pas = api.portal.get_tool("acl_users") if PLUGIN_ID not in pas.objectIds(): - raise ValueError( - "acl_users has no plugin {}.".format(PLUGIN_ID) - ) + raise ValueError(f"acl_users has no plugin {PLUGIN_ID}.") plugin = getattr(pas, PLUGIN_ID) if not isinstance(plugin, OIDCPlugin): - raise ValueError( - "Existing PAS plugin {0} is not a OIDCPlugin.".format(PLUGIN_ID) - ) + raise ValueError(f"Existing PAS plugin {PLUGIN_ID} is not a OIDCPlugin.") # This would activate one interface and deactivate all others: # plugin.manage_activateInterfaces([interface_name]) @@ -87,15 +74,13 @@ def activate_plugin(context, interface_name, move_to_top=False): iface = plugins._getInterfaceFromName(interface_name) if PLUGIN_ID not in plugins.listPluginIds(iface): plugins.activatePlugin(iface, PLUGIN_ID) - logger.info( - "Activated interface %s for plugin %s", interface_name, PLUGIN_ID - ) + logger.info(f"Activated interface {interface_name} for plugin {PLUGIN_ID}") if move_to_top: # Order some plugins to make sure our plugin is at the top. # This is not needed for all plugin interfaces. plugins.movePluginsTop(iface, [PLUGIN_ID]) - logger.info("Moved %s to top of %s.", PLUGIN_ID, interface_name) + logger.info(f"Moved {PLUGIN_ID} to top of {interface_name}.") def activate_challenge_plugin(context): @@ -106,7 +91,7 @@ def uninstall(context): """Uninstall script""" from pas.plugins.oidc.utils import PLUGIN_ID - pas = getToolByName(context, "acl_users") + pas = api.portal.get_tool("acl_users") # Remove plugin if it exists. if PLUGIN_ID not in pas.objectIds(): @@ -115,7 +100,7 @@ def uninstall(context): plugin = getattr(pas, PLUGIN_ID) if not isinstance(plugin, OIDCPlugin): - logger.warning("PAS plugin %s not removed: it is not a OIDCPlugin.", PLUGIN_ID) + logger.warning(f"PAS plugin {PLUGIN_ID} not removed: it is not a OIDCPlugin.") return pas._delObject(PLUGIN_ID) - logger.info("Removed OIDCPlugin %s from acl_users.", PLUGIN_ID) + logger.info(f"Removed OIDCPlugin {PLUGIN_ID} from acl_users.") diff --git a/src/pas/plugins/oidc/testing.py b/src/pas/plugins/oidc/testing.py index 7a76c1f..ce60e41 100644 --- a/src/pas/plugins/oidc/testing.py +++ b/src/pas/plugins/oidc/testing.py @@ -1,7 +1,4 @@ -# -*- coding: utf-8 -*- from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE - -# from plone.app.robotframework.testing import REMOTE_LIBRARY_BUNDLE_FIXTURE from plone.app.testing import applyProfile from plone.app.testing import FunctionalTesting from plone.app.testing import IntegrationTesting @@ -10,11 +7,7 @@ import pas.plugins.oidc -# from plone.testing import z2 - - -class PasPluginsOidcLayer(PloneSandboxLayer): - +class TestLayer(PloneSandboxLayer): defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,) def setUpZope(self, app, configurationContext): @@ -27,26 +20,16 @@ def setUpPloneSite(self, portal): applyProfile(portal, "pas.plugins.oidc:default") -PAS_PLUGINS_OIDC_FIXTURE = PasPluginsOidcLayer() +FIXTURE = TestLayer() -PAS_PLUGINS_OIDC_INTEGRATION_TESTING = IntegrationTesting( - bases=(PAS_PLUGINS_OIDC_FIXTURE,), +INTEGRATION_TESTING = IntegrationTesting( + bases=(FIXTURE,), name="PasPluginsOidcLayer:IntegrationTesting", ) -PAS_PLUGINS_OIDC_FUNCTIONAL_TESTING = FunctionalTesting( - bases=(PAS_PLUGINS_OIDC_FIXTURE,), +FUNCTIONAL_TESTING = FunctionalTesting( + bases=(FIXTURE,), name="PasPluginsOidcLayer:FunctionalTesting", ) - - -# PAS_PLUGINS_OIDC_ACCEPTANCE_TESTING = FunctionalTesting( -# bases=( -# PAS_PLUGINS_OIDC_FIXTURE, -# REMOTE_LIBRARY_BUNDLE_FIXTURE, -# z2.ZSERVER_FIXTURE, -# ), -# name="PasPluginsOidcLayer:AcceptanceTesting", -# ) diff --git a/src/pas/plugins/oidc/tests/test_functional.py b/src/pas/plugins/oidc/tests/test_functional.py deleted file mode 100644 index 2a25c61..0000000 --- a/src/pas/plugins/oidc/tests/test_functional.py +++ /dev/null @@ -1,115 +0,0 @@ -from pas.plugins.oidc.testing import PAS_PLUGINS_OIDC_FUNCTIONAL_TESTING -from plone.testing.zope import Browser -from plone.app.testing import SITE_OWNER_NAME -from plone.app.testing import SITE_OWNER_PASSWORD -from plone.app.testing import TEST_USER_NAME -from plone.app.testing import TEST_USER_PASSWORD - -import unittest - -try: - # Python 3 - from urllib.parse import quote -except ImportError: - # Python 2 - from urllib import quote - - -class TestFunctionalPlugin(unittest.TestCase): - - layer = PAS_PLUGINS_OIDC_FUNCTIONAL_TESTING - - def setUp(self): - self.portal = self.layer["portal"] - self.browser = Browser(self.layer["app"]) - self.browser.handleErrors = False - - def test_challenge_anonymous(self): - self.browser.handleErrors = True - # self.browser.raiseHttpErrors = False - self.browser.followRedirects = False - portal_url = self.portal.absolute_url() - plugin_url = self.portal.acl_users.oidc.absolute_url() - - # Try going to a page for which you need to be authenticated. - url = portal_url + "/@@overview-controlpanel" - quoted_url = quote(url) - self.browser.open(url) - - # Since we have told the browser to not follow redirects, - # we are at the requested location. - self.assertEqual(self.browser.url, url) - # Plone wants to redirect us to the require_login page of the plugin. - location = self.browser.headers["location"] - self.assertEqual(location, plugin_url + "/require_login?came_from=" + url) - - # Follow one redirect. - self.browser.open(location) - self.assertEqual(self.browser.url, location) - # Plone wants to redirect us to the login page of the plugin. - location = self.browser.headers["location"] - self.assertEqual(location, plugin_url + "/login?came_from=" + quoted_url) - - # If we would follow the redirect, we would get a 500 Internal Server Error - # because the login view calls get_oauth2_client on the plugin, - # and we have not setup such a client in the tests. We may need to mock - # some code. - # But the challenge plugin works: it makes sure we end up on the login view - # of our plugin, and not on the standard Plone login form. - - def test_challenge_authenticated_manager(self): - self.browser.addHeader( - "Authorization", - "Basic {0}:{1}".format( - SITE_OWNER_NAME, - SITE_OWNER_PASSWORD, - ), - ) - self.browser.handleErrors = True - # self.browser.raiseHttpErrors = False - self.browser.followRedirects = False - portal_url = self.portal.absolute_url() - - # Try going to a page for which you need to be authenticated. - url = portal_url + "/@@overview-controlpanel" - self.browser.open(url) - - # we are at the requested location. - self.assertEqual(self.browser.url, url) - self.assertEqual(self.browser.headers["status"], "200 OK") - self.assertIn("Site Setup", self.browser.contents) - self.assertIn("Log out", self.browser.contents) - - def test_challenge_authenticated_member(self): - self.browser.addHeader( - "Authorization", - "Basic {0}:{1}".format( - TEST_USER_NAME, - TEST_USER_PASSWORD, - ), - ) - self.browser.handleErrors = True - # self.browser.raiseHttpErrors = False - self.browser.followRedirects = False - portal_url = self.portal.absolute_url() - plugin_url = self.portal.acl_users.oidc.absolute_url() - - # Try going to a page for which you need to be authenticated. - url = portal_url + "/@@overview-controlpanel" - self.browser.open(url) - - # Since we have told the browser to not follow redirects, - # we are at the requested location. - self.assertEqual(self.browser.url, url) - # Plone wants to redirect us to the require_login page of the plugin. - location = self.browser.headers["location"] - self.assertEqual(location, plugin_url + "/require_login?came_from=" + url) - - # Follow one redirect. - self.browser.open(location) - self.assertEqual(self.browser.url, location) - # Plone sees that we are authenticated, but do not have sufficient privileges. - # The plugin does not redirect to the oidc server, because that would likely - # result in login loops. - location = self.browser.headers["location"] - self.assertEqual(location, portal_url + "/insufficient-privileges") diff --git a/src/pas/plugins/oidc/tests/test_plugin.py b/src/pas/plugins/oidc/tests/test_plugin.py deleted file mode 100644 index fa6171e..0000000 --- a/src/pas/plugins/oidc/tests/test_plugin.py +++ /dev/null @@ -1,48 +0,0 @@ -from base64 import b64decode -from oic.oic.message import OpenIDSchema - -# from pas.plugins.oidc.tests.mocks import make_user -from pas.plugins.oidc.setuphandlers import post_install -from pas.plugins.oidc.testing import PAS_PLUGINS_OIDC_INTEGRATION_TESTING -from plone.session.tktauth import splitTicket - -import unittest - - -class TestPlugin(unittest.TestCase): - - layer = PAS_PLUGINS_OIDC_INTEGRATION_TESTING - - def setUp(self): - self.portal = self.layer["portal"] - self.pas = self.portal.acl_users - self.plugin = post_install(self.portal) - self.request = self.layer["request"] - self.response = self.request.response - - def test_remember_identity(self): - userinfo = OpenIDSchema(sub="bob") - # auto create user by default - self.assertTrue(self.plugin.getProperty("create_user")) - # set __ac ticket by default - self.assertTrue(self.plugin.getProperty("create_ticket")) - self.assertIsNone(self.pas.getUserById("bob")) - self.plugin.rememberIdentity(userinfo) - self.assertIsNotNone(self.pas.getUserById("bob")) - self.assertEqual(self.response.status, 200) - self.assertEqual( - splitTicket(b64decode(self.response.cookies["__ac"]["value"]))[1], "bob" - ) - - def test_challenge(self): - request_url = self.request.URL - - # When the plugin makes a challenge, it must return a True value. - self.assertTrue(self.plugin.challenge(self.request, self.response)) - - # Check the response. - plugin_url = self.plugin.absolute_url() - self.assertEqual( - self.response.headers["location"], - "{}/require_login?came_from={}".format(plugin_url, request_url), - ) diff --git a/src/pas/plugins/oidc/tests/test_setup.py b/src/pas/plugins/oidc/tests/test_setup.py deleted file mode 100644 index d6883d8..0000000 --- a/src/pas/plugins/oidc/tests/test_setup.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -"""Setup tests for this package.""" -from pas.plugins.oidc.testing import PAS_PLUGINS_OIDC_INTEGRATION_TESTING # noqa: E501 -from plone import api -from plone.app.testing import setRoles -from plone.app.testing import TEST_USER_ID - -import unittest - - -try: - from Products.CMFPlone.utils import get_installer -except ImportError: - get_installer = None - - -class TestSetup(unittest.TestCase): - """Test that pas.plugins.oidc is properly installed.""" - - layer = PAS_PLUGINS_OIDC_INTEGRATION_TESTING - - def setUp(self): - """Custom shared utility setup for tests.""" - self.portal = self.layer["portal"] - if get_installer: - self.installer = get_installer(self.portal, self.layer["request"]) - else: - self.installer = api.portal.get_tool("portal_quickinstaller") - - def test_product_installed(self): - """Test if pas.plugins.oidc is installed.""" - self.assertTrue(self.installer.is_product_installed("pas.plugins.oidc")) - - def test_browserlayer(self): - """Test that IPasPluginsOidcLayer is registered.""" - from pas.plugins.oidc.interfaces import IPasPluginsOidcLayer - from plone.browserlayer import utils - - self.assertIn(IPasPluginsOidcLayer, utils.registered_layers()) - - -class TestUninstall(unittest.TestCase): - - layer = PAS_PLUGINS_OIDC_INTEGRATION_TESTING - - def setUp(self): - self.portal = self.layer["portal"] - if get_installer: - self.installer = get_installer(self.portal, self.layer["request"]) - else: - self.installer = api.portal.get_tool("portal_quickinstaller") - roles_before = api.user.get_roles(TEST_USER_ID) - setRoles(self.portal, TEST_USER_ID, ["Manager"]) - self.installer.uninstall_product("pas.plugins.oidc") - setRoles(self.portal, TEST_USER_ID, roles_before) - - def test_product_uninstalled(self): - """Test if pas.plugins.oidc is cleanly uninstalled.""" - self.assertFalse(self.installer.is_product_installed("pas.plugins.oidc")) - - def test_browserlayer_removed(self): - """Test that IPasPluginsOidcLayer is removed.""" - from pas.plugins.oidc.interfaces import IPasPluginsOidcLayer - from plone.browserlayer import utils - - self.assertNotIn(IPasPluginsOidcLayer, utils.registered_layers()) diff --git a/src/pas/plugins/oidc/utils.py b/src/pas/plugins/oidc/utils.py index 991e8c6..a8a88a3 100644 --- a/src/pas/plugins/oidc/utils.py +++ b/src/pas/plugins/oidc/utils.py @@ -7,9 +7,6 @@ from oic.oic.message import OPTIONAL_MESSAGE -# from oic.oic.message import SINGLE_OPTIONAL_BOOLEAN - - PLUGIN_ID = "oidc" diff --git a/test_plone43.cfg b/test_plone43.cfg deleted file mode 100644 index a47ed64..0000000 --- a/test_plone43.cfg +++ /dev/null @@ -1,26 +0,0 @@ -[buildout] - -extends = - https://raw.githubusercontent.com/collective/buildout.plonetest/master/test-4.3.x.cfg - https://raw.githubusercontent.com/collective/buildout.plonetest/master/qa.cfg - base.cfg - -update-versions-file = test_plone43.cfg - -[versions] -zc.buildout = -setuptools = -# Pillow = 5.1.0 - -# manual pinnings -collective.recipe.vscode = >=0.1.6 -backports.functools_lru_cache = 1.5 -isort = 4.3.21 -# 2.1.1 is the last supported version for Plone 4.3, but plone.app.contenttypes 1.1.6 is requirering <2.1.0: -plone.app.dexterity = <2.1.0 -docutils = 0.14 -plone.app.robotframework = 1.2.1 -configparser = 3.5.3 -plone.testing = 5.0.0 -jsonschema = 2.6.0 -plone.restapi = 7.8.0 diff --git a/test_plone50.cfg b/test_plone50.cfg deleted file mode 100644 index 8590a73..0000000 --- a/test_plone50.cfg +++ /dev/null @@ -1,13 +0,0 @@ -[buildout] - -extends = - https://raw.githubusercontent.com/collective/buildout.plonetest/master/test-5.0.x.cfg - https://raw.githubusercontent.com/collective/buildout.plonetest/master/qa.cfg - base.cfg - -update-versions-file = test_plone50.cfg - -[versions] -plone.schemaeditor = >=2.0.18 -collective.recipe.vscode = >=0.1.6 -plone.restapi = <8 diff --git a/test_plone51.cfg b/test_plone51.cfg deleted file mode 100644 index 3a6631d..0000000 --- a/test_plone51.cfg +++ /dev/null @@ -1,13 +0,0 @@ -[buildout] - -extends = - https://raw.githubusercontent.com/collective/buildout.plonetest/master/test-5.1.x.cfg - https://raw.githubusercontent.com/collective/buildout.plonetest/master/qa.cfg - base.cfg - -update-versions-file = test_plone51.cfg - -[versions] -plone.testing = 5.0.0 -collective.recipe.vscode = >=0.1.6 -plone.restapi = <8 diff --git a/test_plone52.cfg b/test_plone52.cfg deleted file mode 100644 index 11f5469..0000000 --- a/test_plone52.cfg +++ /dev/null @@ -1,36 +0,0 @@ -[buildout] - -extends = - https://raw.githubusercontent.com/collective/buildout.plonetest/master/test-5.2.x.cfg - https://raw.githubusercontent.com/collective/buildout.plonetest/master/qa.cfg - base.cfg - -update-versions-file = test_plone52.cfg - -[versions] -plone.testing = 7.0.1 -collective.recipe.vscode = >=0.1.6 - -# Added by buildout at 2021-11-11 14:19:02.528833 -Beaker = 1.11.0 -Mako = 1.1.4 -collective.recipe.vscode = 0.1.8 -createcoverage = 1.5 -pyjwkest = 1.4.2 - -# Required by: -# oic==1.3.0 -defusedxml = 0.7.1 - -# Required by: -# pas.plugins.oidc==1.0a2 -oic = 1.3.0 - -# Required by: -# oic==1.3.0 -# pyjwkest==1.4.2 -pycryptodomex = 3.10.1 - -# Required by: -# oic==1.3.0 -typing-extensions = 3.10.0.0 diff --git a/test_plone60.cfg b/test_plone60.cfg deleted file mode 100644 index 58edfa6..0000000 --- a/test_plone60.cfg +++ /dev/null @@ -1,12 +0,0 @@ -[buildout] - -extends = - https://raw.githubusercontent.com/collective/buildout.plonetest/master/test-6.0.x.cfg - https://raw.githubusercontent.com/collective/buildout.plonetest/master/qa.cfg - base.cfg - -update-versions-file = test_plone60.cfg - -[versions] -pygments = 2.14.0 -typing-extensions = 4.6.1 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3e1ab11 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,35 @@ +from pas.plugins.oidc.testing import FUNCTIONAL_TESTING +from pas.plugins.oidc.testing import INTEGRATION_TESTING +from pathlib import Path +from pytest_plone import fixtures_factory + +import pytest + + +pytest_plugins = ["pytest_plone"] + + +globals().update( + fixtures_factory( + ( + (INTEGRATION_TESTING, "integration"), + (FUNCTIONAL_TESTING, "functional"), + ) + ) +) + + +@pytest.fixture(scope="session") +def docker_compose_file(pytestconfig): + """Fixture pointing to the docker-compose file to be used.""" + return Path(str(pytestconfig.rootdir)).resolve() / "tests" / "docker-compose.yml" + + +@pytest.fixture +def wait_for(): + def func(thread): + if not thread: + return + thread.join() + + return func diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 0000000..bb2a4e8 --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,41 @@ +version: "3.9" + +services: + keycloak: + build: + context: keycloak + args: + KEYCLOAK_VERSION: 22.0.0 + command: ['start-dev', '--import-realm'] + depends_on: + - db + environment: + JAVA_OPTS_APPEND: -Dkeycloak.profile.feature.upload_scripts=enabled + KC_DB: postgres + KC_DB_PASSWORD: postgres + KC_DB_URL: jdbc:postgresql://db/keycloak + KC_DB_USERNAME: postgres + KC_HEALTH_ENABLED: false + KC_HTTP_ENABLED: true + KC_METRICS_ENABLED: false + KC_HOSTNAME_URL: http://127.0.0.1:8180/ + KC_PROXY: reencrypt + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + volumes: + - ./keycloak/import:/opt/keycloak/data/import + ports: + - 8180:8080 + + db: + image: postgres:14.9 + healthcheck: + test: [ "CMD", "pg_isready", "-q", "-d", "postgres", "-U", "root" ] + timeout: 45s + interval: 5s + retries: 10 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: keycloak + POSTGRES_HOST: postgres diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py new file mode 100644 index 0000000..161979d --- /dev/null +++ b/tests/functional/conftest.py @@ -0,0 +1,135 @@ +from plone import api +from plone.app.testing import SITE_OWNER_NAME +from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_NAME +from plone.app.testing import TEST_USER_PASSWORD +from plone.restapi.testing import RelativeSession +from plone.testing.zope import Browser +from requests.exceptions import ConnectionError +from zope.component.hooks import setSite + +import pytest +import requests +import transaction + + +def is_responsive(url: str) -> bool: + try: + response = requests.get(url) + if response.status_code == 200: + return True + except ConnectionError: + return False + + +@pytest.fixture(scope="session") +def keycloak_service(docker_ip, docker_services): + """Ensure that keycloak service is up and responsive.""" + # `port_for` takes a container port and returns the corresponding host port + port = docker_services.port_for("keycloak", 8080) + url = f"http://{docker_ip}:{port}" + docker_services.wait_until_responsive( + timeout=30.0, pause=0.1, check=lambda: is_responsive(url) + ) + return url + + +@pytest.fixture() +def app(functional): + return functional["app"] + + +@pytest.fixture(scope="session") +def keycloak(keycloak_service): + return { + "issuer": f"{keycloak_service}/realms/plone-test", + "client_id": "plone", + "client_secret": "12345678", # nosec B105 + "scope": ("openid", "profile", "email"), + } + + +@pytest.fixture() +def portal(functional, keycloak): + portal = functional["portal"] + setSite(portal) + plugin = portal.acl_users.oidc + with api.env.adopt_roles(["Manager", "Member"]): + for key, value in keycloak.items(): + setattr(plugin, key, value) + transaction.commit() + yield portal + with api.env.adopt_roles(["Manager", "Member"]): + for key, value in keycloak.items(): + if key != "scope": + value = "" + setattr(plugin, key, value) + transaction.commit() + + +@pytest.fixture() +def http_request(functional): + return functional["request"] + + +@pytest.fixture() +def request_api_factory(portal): + def factory(): + url = portal.absolute_url() + api_session = RelativeSession(url) + api_session.headers.update({"Accept": "application/json"}) + return api_session + + return factory + + +@pytest.fixture() +def api_anon_request(request_api_factory): + return request_api_factory() + + +@pytest.fixture() +def api_user_request(request_api_factory): + request = request_api_factory() + request.auth = (TEST_USER_NAME, TEST_USER_PASSWORD) + yield request + request.auth = () + + +@pytest.fixture() +def api_manager_request(request_api_factory): + request = request_api_factory() + request.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + yield request + request.auth = () + + +@pytest.fixture() +def browser_factory(app): + def factory(): + browser = Browser(app) + browser.handleErrors = False + browser.followRedirects = False + return browser + + return factory + + +@pytest.fixture() +def browser_anonymous(browser_factory): + browser = browser_factory() + return browser + + +@pytest.fixture() +def browser_user(browser_factory): + browser = browser_factory() + browser.addHeader("Authorization", f"Basic {TEST_USER_NAME}:{TEST_USER_PASSWORD}") + return browser + + +@pytest.fixture() +def browser_manager(browser_factory): + browser = browser_factory() + browser.addHeader("Authorization", f"Basic {SITE_OWNER_NAME}:{SITE_OWNER_PASSWORD}") + return browser diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py new file mode 100644 index 0000000..d98f50d --- /dev/null +++ b/tests/functional/test_functional.py @@ -0,0 +1,79 @@ +from pas.plugins.oidc.utils import PLUGIN_ID +from plone import api +from urllib.parse import quote + +import pytest + + +class TestFunctionalPlugin: + @pytest.fixture(autouse=True) + def _initialize(self, portal): + pas = api.portal.get_tool("acl_users") + plugin = getattr(pas, PLUGIN_ID) + self.portal_url = api.portal.get().absolute_url() + self.plugin_url = plugin.absolute_url() + + def test_challenge_anonymous(self, browser_anonymous): + browser = browser_anonymous + browser.handleErrors = True + url = f"{self.portal_url}/@@overview-controlpanel" + quoted_url = quote(url) + browser.open(url) + # Since we have told the browser to not follow redirects, + # we are at the requested location. + assert browser.url == url + # Plone wants to redirect us to the require_login page of the plugin. + location = browser.headers["location"] + expected = f"{self.plugin_url}/require_login?came_from={url}" + assert location == expected + + # Follow one redirect. + browser.open(location) + assert browser.url == location + # Plone wants to redirect us to the login page of the plugin. + location = browser.headers["location"] + expected = f"{self.plugin_url}/login?came_from={quoted_url}" + assert location == expected + # Follow one redirect, view the login page of the plugin + browser.handleErrors = False + browser.open(location) + assert browser.url == location + # The next redirect will send us to keycloak + location = browser.headers["location"] + expected = "http://127.0.0.1:8180/realms/plone-test/protocol/openid-connect/auth?client_id=plone" + assert location.startswith(expected) + + def test_challenge_authenticated_user(self, browser_user): + browser = browser_user + browser.handleErrors = True + url = f"{self.portal_url}/@@overview-controlpanel" + browser.open(url) + # Since we have told the browser to not follow redirects, + # we are at the requested location. + assert browser.url == url + # Plone wants to redirect us to the require_login page of the plugin. + location = browser.headers["location"] + expected = f"{self.plugin_url}/require_login?came_from={url}" + assert location == expected + + # Follow one redirect. + browser.open(location) + assert browser.url == location + # Plone sees that we are authenticated, but do not have sufficient privileges. + # The plugin does not redirect to the oidc server, because that would likely + # result in login loops. + location = browser.headers["location"] + expected = f"{self.portal_url}/insufficient-privileges" + assert location == expected + + def test_challenge_authenticated_manager(self, browser_manager): + browser = browser_manager + browser.handleErrors = True + portal_url = self.portal_url + # Try going to a page for which you need to be authenticated. + url = f"{portal_url}/@@overview-controlpanel" + browser.open(url) + assert browser.url == url + assert browser.headers["status"] == "200 OK" + assert "Site Setup" in browser.contents + assert "Log out" in browser.contents diff --git a/tests/keycloak/Dockerfile b/tests/keycloak/Dockerfile new file mode 100644 index 0000000..20a72d1 --- /dev/null +++ b/tests/keycloak/Dockerfile @@ -0,0 +1,30 @@ +# syntax=docker/dockerfile:1 +ARG KEYCLOAK_VERSION +FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION as builder + +# Configure postgres database vendor +ENV KC_DB=postgres + +# Disable health and metrics support +ENV KC_HEALTH_ENABLED=false +ENV KC_METRICS_ENABLED=false + +# Enable features +ENV KC_FEATURES="token-exchange,scripts,preview,admin_fine_grained_authz" + +WORKDIR /opt/keycloak + +# Build +RUN /opt/keycloak/bin/kc.sh build --cache=ispn + +FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION +LABEL image.version=$KEYCLOAK_VERSION +COPY --from=builder /opt/keycloak/ /opt/keycloak/ + +USER root +RUN sed -i '/disabledAlgorithms/ s/ SHA1,//' /etc/crypto-policies/back-ends/java.config +USER keycloak + +RUN /opt/keycloak/bin/kc.sh show-config + +ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] diff --git a/tests/keycloak/import/.gitkeep b/tests/keycloak/import/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/keycloak/import/plone-realm.json b/tests/keycloak/import/plone-realm.json new file mode 100644 index 0000000..7eca1ef --- /dev/null +++ b/tests/keycloak/import/plone-realm.json @@ -0,0 +1,1792 @@ +{ + "id" : "a32aee6c-4b76-4e17-9e2d-818fa320d2e8", + "realm" : "plone", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "113d7ad2-5728-4403-b071-3dbe81f26f4b", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "a32aee6c-4b76-4e17-9e2d-818fa320d2e8", + "attributes" : { } + }, { + "id" : "9257df49-1169-4438-9e5d-be5b147b8677", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "a32aee6c-4b76-4e17-9e2d-818fa320d2e8", + "attributes" : { } + }, { + "id" : "394bfcd9-b2b1-42d8-9700-cb0c473a8161", + "name" : "default-roles-plone", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "view-profile", "manage-account" ] + } + }, + "clientRole" : false, + "containerId" : "a32aee6c-4b76-4e17-9e2d-818fa320d2e8", + "attributes" : { } + } ], + "client" : { + "realm-management" : [ { + "id" : "08d003dc-d4db-4bb4-93c8-24ca19be625e", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "0bd720a9-94a2-42bc-a6f1-be3c60bcd6fa", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "c347c137-e81c-4772-87f4-5ec13cfe6806", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "4054aa05-6b04-4793-9e1f-c3416b31ed55", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "51fa057a-59de-44e9-bc68-1b56f6b7a5ad", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "504a2b3d-d6e5-4c24-b087-b17356ee789d", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "f050953e-5d92-4983-9006-75a8131487c3", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "920355ce-2458-4fd9-bcdd-0be26b4b4f06", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-users", "query-groups" ] + } + }, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "8a377508-715a-45b6-a59d-b88de2fba741", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "341a70e3-2dbc-404b-976e-c54b3cf96b1d", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "d9c159ec-f3d9-401c-bd68-f68184235541", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "a9986730-84db-4b05-88ec-aff6f7a256ca", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "create-client", "manage-events", "manage-users", "query-groups", "manage-authorization", "query-clients", "view-realm", "view-users", "view-events", "view-clients", "manage-clients", "query-users", "query-realms", "manage-identity-providers", "manage-realm", "impersonation", "view-authorization", "view-identity-providers" ] + } + }, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "98440c92-091a-4663-9d3f-449793fa8195", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "cdf459bf-5eb7-4d5b-b824-7d742fae9d6b", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "c8d09e3b-0879-4543-9140-60cedf141c1e", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "136a6670-bc88-4e0b-8296-406e4cae7f58", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "97586af6-01ad-4bef-a75c-7f52d09c7ca8", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "cfdd3ea9-fa8c-48f8-abf7-92b561fbfa02", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + }, { + "id" : "557c2c5d-378c-40f4-b3d8-193f77a81a52", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "plone" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "96f4ee11-2020-428a-989f-6a1e9aec8c73", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "187bbb7b-dcc6-4fec-89e4-bbe455453ab7", + "attributes" : { } + } ], + "account" : [ { + "id" : "4fb679d6-e349-4b7f-884d-1ecd742732a0", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "9f21ddf3-ab2c-4c9c-8483-4e05ba71725c", + "attributes" : { } + }, { + "id" : "37624829-5d0c-4f8b-bbaa-62bcfe87e490", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "9f21ddf3-ab2c-4c9c-8483-4e05ba71725c", + "attributes" : { } + }, { + "id" : "0378916b-dfd4-42d0-8db3-0f54362e0bde", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "9f21ddf3-ab2c-4c9c-8483-4e05ba71725c", + "attributes" : { } + }, { + "id" : "eb9a2848-85fe-4e92-b3af-300b938cee84", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "9f21ddf3-ab2c-4c9c-8483-4e05ba71725c", + "attributes" : { } + }, { + "id" : "55de7b5f-4761-4428-86ef-836353d1a6cd", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "9f21ddf3-ab2c-4c9c-8483-4e05ba71725c", + "attributes" : { } + }, { + "id" : "40c2a677-690b-4e82-aa98-1e2bc1a3cd36", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "9f21ddf3-ab2c-4c9c-8483-4e05ba71725c", + "attributes" : { } + }, { + "id" : "a465efa6-c89f-4b7b-bea8-8ed68c482253", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "9f21ddf3-ab2c-4c9c-8483-4e05ba71725c", + "attributes" : { } + }, { + "id" : "411d3818-5297-4ce7-b873-ac4426ab7223", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "9f21ddf3-ab2c-4c9c-8483-4e05ba71725c", + "attributes" : { } + } ] + } + }, + "groups" : [ ], + "defaultRole" : { + "id" : "394bfcd9-b2b1-42d8-9700-cb0c473a8161", + "name" : "default-roles-plone", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "a32aee6c-4b76-4e17-9e2d-818fa320d2e8" + }, + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName", "totpAppFreeOTPName" ], + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "users" : [ { + "id" : "256b08a1-5830-46b3-89fd-9475b0c12b61", + "createdTimestamp" : 1699479231659, + "username" : "user", + "enabled" : true, + "totp" : false, + "emailVerified" : true, + "firstName" : "Plone", + "lastName" : "User", + "email" : "collective@plone.org", + "credentials" : [ { + "id" : "ae7306c8-2bdf-4c32-b7ac-bd4b9953093f", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1699479246196, + "secretData" : "{\"value\":\"6//sMXjSc+Q+kFqTLRgK6ihZHP/HyKpJOkUdAcvCTXk=\",\"salt\":\"NussvDaHepLB7Fcg+Hrnqg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-plone" ], + "notBefore" : 0, + "groups" : [ ] + } ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account", "view-groups" ] + } ] + }, + "clients" : [ { + "id" : "9f21ddf3-ab2c-4c9c-8483-4e05ba71725c", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/plone/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/plone/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "3f199b5f-8196-48fc-baae-271819ecb232", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/plone/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/plone/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "e6afb514-7627-45d1-a4dc-603cf5925aef", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "62340d52-ab99-44a8-adc6-8645c7895fd5", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "187bbb7b-dcc6-4fec-89e4-bbe455453ab7", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "fddddd26-23b1-4859-8582-0f5bea4bcf49", + "clientId" : "plone", + "name" : "plone", + "description" : "", + "rootUrl" : "http://localhost:8080/Plone/", + "adminUrl" : "http://localhost:8080/Plone/", + "baseUrl" : "http://localhost:8080/Plone/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "12345678", + "redirectUris" : [ "http://localhost:8080/Plone/*" ], + "webOrigins" : [ "http://localhost:8080/Plone/" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "oauth2.device.authorization.grant.enabled" : "false", + "client.secret.creation.time" : "1699479200", + "backchannel.logout.session.required" : "true", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "cb6cd450-a2bf-4d1f-8ccb-891fd8ad5890", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "93dccf39-f189-4826-9e93-6a10fc50d335", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/plone/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/plone/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "11c69999-4ec1-4639-978d-477e6d520e19", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "49637aa4-aac7-442a-bac3-0a3470f1bd8d", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "d8465a0d-c12d-4215-bd07-8593605d849a", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + }, { + "id" : "0f7163d0-e279-4f8e-b5de-d754f8dcc2c5", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "eb461eb3-e24e-4dec-b490-baa6e37d25b1", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "9b014380-0855-466a-be69-23cc865830ee", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${rolesScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "0f8a3b71-ed59-4adb-8d70-9a4b3fff665e", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "ca48487d-1e23-49a7-96a2-79c6f4c8b222", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "17b49788-9059-4898-820c-13a487240dc2", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "bc52d63a-11c3-4ae6-870b-deadd4fa53fe", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${profileScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "9a1b6758-a117-4ba2-8156-90c51729aada", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "1b3421c5-5879-46af-832e-7a758f1a75cb", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "887bfac8-a9f8-4d49-8405-9fe738f7fc75", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + }, { + "id" : "df4ecd6d-4e67-4e48-af6b-a05f7981c126", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + }, { + "id" : "992d2e80-bfc6-49dc-837f-97444dccb1a0", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "5996f4b4-7760-4416-8d41-b729e459d739", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "ad9cf902-04f9-41ca-b3bd-edc7fd6f6e95", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "297df195-f18a-4674-a74b-6ffcbae7ec96", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "8c01c6b4-152a-421e-9e1c-68527aeaca04", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "a0902192-3380-45cc-b39b-536ad4d59fe6", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "c69da6c1-d131-472f-b598-31934cfdc31d", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "f844a5e0-37e7-482e-bd7f-1b9342e1ce06", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "0f86faff-1f78-4c30-94a8-f1773298828d", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "4fc7b25c-67f8-4361-9e69-a25532da9d7d", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + } ] + }, { + "id" : "c1a4dbec-9ba0-4581-8e79-427b0ae16d7c", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false", + "consent.screen.text" : "" + }, + "protocolMappers" : [ { + "id" : "d65a0232-c604-4c1e-a9f1-ae0fc1a8da46", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "97b199e5-619b-4918-9d6c-74237095ad10", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${addressScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "e0734341-a36f-42ff-9d31-7ed25fe1683f", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + }, { + "id" : "c14a40cb-2652-4ab8-ba34-81cab650db57", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${phoneScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "1e50d9a5-1fcd-4208-888a-91174c72d7aa", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + }, { + "id" : "2b0d4dbf-2251-42ce-876e-f1fa3afcdbe5", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "425379c2-a7ef-4411-9eb9-09e52a30201d", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "8cf04d3f-e236-46d4-abe6-b81090787f43", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + }, { + "id" : "60ae47fe-f510-4992-a3c5-4e82a3aebb72", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${emailScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "67148842-3e05-4368-9e12-5b6c65a78dec", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "9c2a1bce-f607-4aa8-97f7-a41f67e68a86", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "b5934f91-71c4-4f27-8052-567c73f7b002", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "7ba529dd-2816-453a-878a-22b52c26c670", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "referrerPolicy" : "no-referrer", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "0be03525-fb8e-4cf5-a769-0008058464a6", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "544e45aa-8249-4df0-b386-aa7c66850f8b", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper" ] + } + }, { + "id" : "27b6e52b-b984-4848-b7a1-8486cca0e969", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "e03fc566-fcec-4a9a-8b84-d41aefb7c2be", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "b68fea24-8631-4fa1-94ed-aace143a8646", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "a619b744-7f98-4ed3-8862-d015e5862e57", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-address-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper" ] + } + }, { + "id" : "06fee4a7-63c4-4dc8-b229-18b3d231ed6a", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "5ed393be-1291-46bb-9394-0ce6d3b9e44d", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "a7f370c1-e6f4-4bd8-b427-51695bbf01d5", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "23a88d1d-fe44-4e64-8498-ac9b67a6f920" ], + "secret" : [ "45Z1aMwKRU7oIC-RvgCr80upNSgu83M0uN2slKTYNH5P0ShS4IwwA6-lG4sZbyxJtgeLwRI1_cos-A-0xy27Fg" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + }, { + "id" : "5031463c-86c4-4ae3-9369-a9f4ae9caf1b", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "df070548-5e51-4c4e-9313-393e4bf02646" ], + "secret" : [ "q0WBfklc8U9XOcplbNL4wQ" ], + "priority" : [ "100" ] + } + }, { + "id" : "14d9e6fb-b83e-4bc6-a26f-7bb3966709ad", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEAvB6IQF2V6ZEHwX0OinKI4pCTBvW1nYjLkrlhRfsOYvNym5InNVzPh1WnAz55g+xNJepuihQtkoarrWRJzYhlss+RMCiD40qKuqT2KK63JDVzfVpTxyVWqroVTpMcpp2R2u3a3MmHe7kipfEf3mug0PbJgc1X8I6rxOizMnfEJwXyokVn9efYZDEtjT+j6hFrtAsg1FUj6rgOLpqY0SX+xfuZIbvQ9nnFRSFY7b/R07HhbuVMg4aoqPFNtBdW5X995MqAcWT7uKYGCULaXtBM/K5n47dgX2soUf3WwKdiT4Ezj6TmstgDH02Om4k9JjHSj47JD6fnMYe6S1wdEDg0+wIDAQABAoIBAEZOjPKtZ2FfPExSvlP9AW7XxLN1snZZkWfZU+MRNOvDe8Jr6a52bPNyDhTWzRiCgMTabIy7hNq97TzE/hZMU2H0VJlwkvYoPlgKXhuPTNBCD9M6JpSHvm/ZUyxGLA+TVVdCzSj2QuX5J9toFlaM2BIxIdea1LKJx+W0aIDEpTFF0L4bGvjeas9TMQKoaBEiKV/6KT9kW49SNHnco/x/A1UbQxTyj7hnUHue/JAZyqsDVGpwypQ/zfFXcv18wKZmmXVS4ebtkgz8f9lI9Sp696t+7t5Gv1A8uMnTYRTdwxr2NDKDL6z70yHQFnVLwKmB982udEexmluqMCU1pl9ijkkCgYEA+244xbc59wU65/y9+XFNm6nSWwaSfZRIMfD+XGK6ucm0yeApyVEM/vmrY6mOa072oeMPZVzW2icCTx0ZtSiGGKA15PNuxy+pn/MWpLOsRBoIHEp4f8WNabgOUvVX9/ygMG0FoUVCy6iYm8yIAKEcqK6Nf+n48iNVmtEvVhFO9xkCgYEAv4nBXAO6LiGdjGx4QHLgz5rPmXzeL3Z4A8oV+sr60M1yUCPFX1IyfdgJg5Tt1mgSoCjO6m1k4ZtKaENOZsiRXD0GiiI3b1Cui58A+1EpBMSK0FgZQAaUaNgcgAjB+NjDPnZc4TRVWZpYUnw0LxTXBlC17dLOtuALrEYclNu7MzMCgYAGA/0XveqFHnrqUgG5lhxlPQrrYpzepE2V5E4xp6sQBuNscRCO4IGPGCohmEzwBFOiS+xyYGZDketcOTYEP5P1wt+HOsvLuDg2u3ovNp1Ig9bt1lpLtE5N2Tl+gqA4LrMmdXVNBnsypDYChEI8wZ4N6x83kEGEiZPiwF6BkKhzAQKBgDRrbw/PtVSKvqN/zClXR0+J/uby1ZwHwDDJTdqlUp9ymqZ0I8BCrq7iQThRugXD1dUfVgGyjrlv1p1STlKy8ZVc3j1Pv/wnCJ4Nlicxvxge8h6y/bYYFuXav3IK84s3R7gAjMrVl8Y+B44Lj/ySp4aC8Ed2OjyydH9hcNEMkd8hAoGBAPT/gHQyM1Z6VEShwMKStBsn9PtnhMVAg6yrO9NYp93oUCtqGg1kyUsqQiWLvvtGkZtOTH/51i0ADKW62trud7noAIzb4T25ajyC69fbsIJYWqD13L4DrqcQp3v8bccWhBNn2p6Ukwvd7qkFjoWYKvRfSSu4WT8HMlwLcPVr4nJ/" ], + "keyUse" : [ "SIG" ], + "certificate" : [ "MIICmTCCAYECBgGLsNm1eTANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVwbG9uZTAeFw0yMzExMDgyMTMwMzlaFw0zMzExMDgyMTMyMTlaMBAxDjAMBgNVBAMMBXBsb25lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvB6IQF2V6ZEHwX0OinKI4pCTBvW1nYjLkrlhRfsOYvNym5InNVzPh1WnAz55g+xNJepuihQtkoarrWRJzYhlss+RMCiD40qKuqT2KK63JDVzfVpTxyVWqroVTpMcpp2R2u3a3MmHe7kipfEf3mug0PbJgc1X8I6rxOizMnfEJwXyokVn9efYZDEtjT+j6hFrtAsg1FUj6rgOLpqY0SX+xfuZIbvQ9nnFRSFY7b/R07HhbuVMg4aoqPFNtBdW5X995MqAcWT7uKYGCULaXtBM/K5n47dgX2soUf3WwKdiT4Ezj6TmstgDH02Om4k9JjHSj47JD6fnMYe6S1wdEDg0+wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCKvvEVMHsxkJDGLAB5pyx0zVfVZ0WGh+gLebtC9q5PXz2XQc6+u9J0xmm5xkF42kR646OjCuq3kB3oQmGH3EvI13A/H0amaQHJoVG3460ZrUr7VjN2Yhfb9ifS5o/+7chr92HPuTXJHWFpAK3hXurVfOKY9Xbyshs6JD1SNHRX5+eXKXOgsGRlb9AXdg9d3JFc2JRvp8VQKroFs0eZzLqF8a7LH9nmNLfuicd4QUhqrdp/K7D0beVmYZCKSjkxVf46/uEDvZjHkc+ahWZo6rdSAXujptLU8Rhtgq+/uQkbWjA4yqeewPAFJU4eagrsA006vfWU/9rO9iggOmbLaxtL" ], + "priority" : [ "100" ] + } + }, { + "id" : "55622799-f899-49a2-ab46-2ac66098bfa1", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEArNirkbYGdSyjsqGU3c2wotSGyQItfKM2F/d1lJNRNV+R0qF+8gEgNjSW5DrzeWdzjTQWssf5IRiwd8pQJoe7w9LOqdU8i7p/rU/Y3DY3tl/Dn+e5t+MzJtRAi4MmjR22iZDsEMk/I/WftU6K8/0Fnjr8kYs7/wpSEBkYsd97YPaJ8dgMc1YIXbCzYG/KCucdAIX7PJkbE5VLk4ZCi4/jLLJya0SEEkWrQyEcdq4OowKq31TjS1ekpnwZwN2lxn4G7djAbD2lOLt6HxZtyQ12DV2NLGFjswZFfsPvhqmvMGVbJw8NhgJ7gugZPQpeOAAk+oguSVSAQMRYhi+wLnesEQIDAQABAoIBAA50BT+3UvmqGIujvYshC6XIA/fLt/JBwxCUu5cnjO07vzm30aHKpI8HzXzK7ahPlV7ftcywElXZRstThml8ZN/qcSfMKgaMmyfKwxlh6r10K0tkIPhelhzHd5i66Ca5bvW4obbDH0WiYL8UUOP3WsiJCezhMcyBKEzKjnE0PPDF9aEaVY2euLxibMHUwZqRgm+aRC/UQgpyur/er+RXtyOUJrHn+YOGzhx5AlMXakmD2Q3BnFloA8yrAts0Csq3ar1L/WbHJUq0s2V5EH1nUWdAeof15Rh+WZMZZ0X9QCLNZbMwO6QxcgCAYwEGxya3rphkd5p9GP53lxHJQKbRWkkCgYEA14e+Lx451AlfUQJ1Ntd4Sk+rIvAHdd42vcl6iYsac7NpymAyOo2Qlf/8JJdPSxOIQF01Dimv9CxsXOrLYspvXeyZeuPS1MXKUqrI+OO1ixproyxeyy7N+AizQBnagXL3YbcTtaGY6mOZ3eCmIwwqfXXimP0GusfvGs0hyV/cn6kCgYEAzU0rSZzHxm3Z9twbUCUaldccOD1pqH4A1HmW2s3Tvx6ZDnAwLqVc/bSPntwSSuCe0h2cZaQoXalQ4qiudnOeVnlhM0RLQWk4To1HbSeL6HhKXy+EIzveMAWbgit19FTdLvNQ9gSDQChDLYgdVX00+kxV74FfnHctMM/pZzQpiikCgYBcqx6Yf7R5eHYlDJt9MvOjb9GzOeTbRunefAxVqxt6d7+apfWyuz9ZXzD4Qp5zNzi0A99V8ZKXt6n34XQFS+jKR3pOHjp+BXTSSmASs0VRQ9DX3bH8RmmEpaj8fKE4W0gGGYM96vcmgCCeB4MnOoniW5dMFC6ajHA/48cNIExp0QKBgGbn8XS6g2CxG3ojpgUsNAE7i8ec8OMmFaBrOfR/C05XNf0Iuv4GoXACG6NTSFAD+1fyw7wm5pzcC9ExIBH7rjhGKzSZXOdyqGinZFAQbI6gDRddJ+zJS3vis6oxwrQv/ZZVHBXU+T0lcVbXTWzWj8GOldyxio1RJ3O6I5I5kUDRAoGBAMOxDPvonNjyP/EJq120rZ+APQZV82O2b7lwT0P8cuovI6zhge+hbe9mMCwnJGYHbTaUJGc0DVNGwPcf8wX9q3KcCDngYQFVNetFAwkzVkT1BkeKaqgmKsJ6mqVW/s+3DTec3Zcm21VBBCtrO3WD6KlulIBCcRzne+Qt3tXEd+y6" ], + "keyUse" : [ "ENC" ], + "certificate" : [ "MIICmTCCAYECBgGLsNm13jANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVwbG9uZTAeFw0yMzExMDgyMTMwMzlaFw0zMzExMDgyMTMyMTlaMBAxDjAMBgNVBAMMBXBsb25lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArNirkbYGdSyjsqGU3c2wotSGyQItfKM2F/d1lJNRNV+R0qF+8gEgNjSW5DrzeWdzjTQWssf5IRiwd8pQJoe7w9LOqdU8i7p/rU/Y3DY3tl/Dn+e5t+MzJtRAi4MmjR22iZDsEMk/I/WftU6K8/0Fnjr8kYs7/wpSEBkYsd97YPaJ8dgMc1YIXbCzYG/KCucdAIX7PJkbE5VLk4ZCi4/jLLJya0SEEkWrQyEcdq4OowKq31TjS1ekpnwZwN2lxn4G7djAbD2lOLt6HxZtyQ12DV2NLGFjswZFfsPvhqmvMGVbJw8NhgJ7gugZPQpeOAAk+oguSVSAQMRYhi+wLnesEQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBPodIuzXjSCKZj1gcDdcwykV3c9/dSvloRtE0WcPUnurE5zcGn5ft0J72HV0P0sQuIJ4DOoCoCgNLVMnPIGHiIZyfkwjqNIc0jooBUr4nUbx7UjpQm92dFH7nmAb7SwuC4ePVtDxZXrODkSRZ39yKN88Cu+5m09Cy1yjkKbsDOsldmnaR07fZ4/iBWiSZjEVnAZnccgpf0qVBKJsCIuoptYy/pT9SHQdx14RaKpzFpkOMAvzlaEhQwG8a1WvlEVlvkmf35SEJIP/6oXPwW/ioInlX3ITnQQAW/Rx5HWRK/GbMlGYaRUQwQHbTkwsp/TY9EX+7GH118aMjcihtvl9UW" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "2e59539c-f5b6-4259-a83e-8107747419fe", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "49939fec-e807-4604-adad-5bd65e9c5033", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "81681f54-df7a-4645-a064-f196aaf8d833", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "1f44db4a-3cb3-4f5a-9fc6-5bc62a853192", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "b486454e-e38b-4742-b563-2ba73127a813", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "7d8415ec-d82e-4762-8399-6011095729d9", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "374447b6-4849-4bb6-9585-86a406ebcc29", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "24fb61ba-7aed-4acd-bea9-6ee0bb87b358", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "e8e6670d-47ab-46d1-8397-c5167f9f58a8", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "56edc7b3-bbc5-4354-a115-4e8f90412b8e", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "affe054c-96ba-491f-bd0c-49511fc5095d", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "39429a8b-0dc6-4785-922d-177e5f7c6523", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "f1f7563c-9bf4-4d93-90d7-946fe126b76f", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "40da5bd3-388c-4e5f-9d86-481299b06369", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "4ae0fd20-c387-4a7b-b240-3d2fe744baef", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "acb5f2fe-bdc6-44c7-94e7-fe3c2b8b9bed", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-profile-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "3539396d-8a24-431b-8ef3-d5172b4783e0", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "1e282d2f-c16d-4e8b-97d1-7570ce650cda", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "6b64147c-37b6-45b9-81a9-a89b40cceeb1", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "fbcfc7b9-59dd-467f-9f14-445843f9b399", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "TERMS_AND_CONDITIONS", + "name" : "Terms and Conditions", + "providerId" : "TERMS_AND_CONDITIONS", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "webauthn-register", + "name" : "Webauthn Register", + "providerId" : "webauthn-register", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "webauthn-register-passwordless", + "name" : "Webauthn Register Passwordless", + "providerId" : "webauthn-register-passwordless", + "enabled" : true, + "defaultAction" : false, + "priority" : 80, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaExpiresIn" : "120", + "cibaAuthRequestedUserHint" : "login_hint", + "oauth2DeviceCodeLifespan" : "600", + "oauth2DevicePollingInterval" : "5", + "parRequestUriLifespan" : "60", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false" + }, + "keycloakVersion" : "22.0.0", + "userManagedAccessAllowed" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } +} diff --git a/tests/keycloak/import/plone-test-realm.json b/tests/keycloak/import/plone-test-realm.json new file mode 100644 index 0000000..5beb0b0 --- /dev/null +++ b/tests/keycloak/import/plone-test-realm.json @@ -0,0 +1,1830 @@ +{ + "id" : "c6db8ff8-ad1f-4d37-86ca-063669c0d777", + "realm" : "plone-test", + "displayName" : "Plone", + "displayNameHtml" : "", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "00e175d1-7d3a-4579-9ec6-25ce35ad7542", + "name" : "default-roles-plone", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "manage-account", "view-profile" ] + } + }, + "clientRole" : false, + "containerId" : "d6db8ff5-ad1f-4d37-86ca-063669c0e727", + "attributes" : { } + }, { + "id" : "f0aa7fbe-2cc1-497d-acfd-7559c51a6ab9", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "d6db8ff5-ad1f-4d37-86ca-063669c0e727", + "attributes" : { } + }, { + "id" : "88014317-446d-49a0-ad13-da245c2b9608", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "d6db8ff5-ad1f-4d37-86ca-063669c0e727", + "attributes" : { } + } ], + "client" : { + "realm-management" : [ { + "id" : "aea52cbe-ffaa-49dd-9d94-75d6b71df800", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "ecfbf26e-10d2-42a6-943e-202bb4c5074d", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-groups", "view-events", "view-identity-providers", "impersonation", "manage-authorization", "view-authorization", "manage-realm", "create-client", "query-users", "manage-users", "view-clients", "view-realm", "view-users", "query-clients", "manage-events", "manage-identity-providers", "query-realms", "manage-clients" ] + } + }, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "31504c52-d1dd-4a60-b049-124d8e6f5f49", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "c0f03823-50cd-4037-8f2b-fefa97a0cca2", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "bf9d6a30-a8c8-4a4a-a494-b84a3ca71a6e", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "d82c6496-10cc-43f1-9173-97eeb91f11dc", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "c6129092-8447-4148-99e8-48f2c40636a1", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "9226842e-bfb2-4cc7-b7fc-102cfe2a5c28", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "2cf39ee5-f5bf-4f73-b258-3853564213c0", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "bb4ade51-47c1-48fe-ad8e-ddb3f00569a4", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "44dd983b-fe94-483e-a480-2431889f7c9f", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "ce7e9216-6dd3-437a-8836-fcc933ac3242", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "2a9862aa-4e70-4cc9-bdde-f71f1090fb44", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "a5c2b411-c343-44bb-b701-b81fe71e017f", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-groups", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "c4e9dddb-509e-475f-b99e-fcdd55f20b94", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "8de7f5fa-64bf-43a0-bd7e-1ba732debfdc", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "f7bbe27c-79cb-4e94-b30a-736de170206d", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "a6c8cd88-64d1-487f-b799-57b7254d99e9", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + }, { + "id" : "c68be57d-33de-4777-b947-4f3f620b0744", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "plone" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "0118bf02-a15a-4dfb-8f14-d2108af94731", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "4ff856b5-f400-44fa-8baa-03d34e154032", + "attributes" : { } + } ], + "account" : [ { + "id" : "691cd0ef-c001-48bb-ac72-05fefae5246c", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "f2f26b12-dea3-495f-9761-b7dcbdb2d50a", + "attributes" : { } + }, { + "id" : "bcab9eaa-0a9c-4d32-8e5e-49403649b9fa", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "f2f26b12-dea3-495f-9761-b7dcbdb2d50a", + "attributes" : { } + }, { + "id" : "c43d34ae-6600-4a9c-8d49-ef7164d38f58", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "f2f26b12-dea3-495f-9761-b7dcbdb2d50a", + "attributes" : { } + }, { + "id" : "f03fb5aa-625b-4034-9f61-7fdbc70ac6cb", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "f2f26b12-dea3-495f-9761-b7dcbdb2d50a", + "attributes" : { } + }, { + "id" : "0ceb15ec-819f-4fe6-bcfc-290012d676eb", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "f2f26b12-dea3-495f-9761-b7dcbdb2d50a", + "attributes" : { } + }, { + "id" : "2510d3bb-b184-4621-8d63-ffa19608c0c1", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "f2f26b12-dea3-495f-9761-b7dcbdb2d50a", + "attributes" : { } + }, { + "id" : "7fbbc7b8-6518-4805-bd35-b1e05d7b8692", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "f2f26b12-dea3-495f-9761-b7dcbdb2d50a", + "attributes" : { } + }, { + "id" : "e95786c7-9ed7-4183-8ce7-879722eec460", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "f2f26b12-dea3-495f-9761-b7dcbdb2d50a", + "attributes" : { } + } ] + } + }, + "groups" : [ ], + "defaultRole" : { + "id" : "00e175d1-7d3a-4579-9ec6-25ce35ad7542", + "name" : "default-roles-plone", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "d6db8ff5-ad1f-4d37-86ca-063669c0e727" + }, + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppMicrosoftAuthenticatorName", "totpAppFreeOTPName", "totpAppGoogleName" ], + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "users" : [ { + "id" : "adf05305-9b27-4fad-b633-23d21ef32431", + "createdTimestamp" : 1699477447820, + "username" : "test-user", + "enabled" : true, + "totp" : false, + "emailVerified" : true, + "firstName" : "Test", + "lastName" : "User", + "email" : "test_user_1_@plone.org", + "credentials" : [ { + "id" : "2c25c4ee-c61b-41a5-bb23-b7d7e9b87285", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1699477473484, + "secretData" : "{\"value\":\"zzjRuxHvkt5xWZ9fDCX68cSAT7bREoE0RuYm+Mh/UiI=\",\"salt\":\"GQnmIEDCdSDXPUidfgx2Uw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-plone" ], + "notBefore" : 0, + "groups" : [ ] + } ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account", "view-groups" ] + } ] + }, + "clients" : [ { + "id" : "f2f26b12-dea3-495f-9761-b7dcbdb2d50a", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/plone/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/plone/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "3eeffc97-14ef-4e78-814f-861efb720549", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/plone/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/plone/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "7cc9be09-a5c2-4179-aee4-3e76c7a40256", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "110e2f50-e1e7-4cac-97ca-70001f35fab4", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "4ff856b5-f400-44fa-8baa-03d34e154032", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "91054dae-29b7-4ce5-b8c4-74a027a46652", + "clientId" : "plone", + "name" : "plone", + "description" : "", + "rootUrl" : "http://nohost/plone/", + "adminUrl" : "http://nohost/plone/", + "baseUrl" : "http://nohost/plone/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "12345678", + "redirectUris" : [ "http://nohost/plone/*" ], + "webOrigins" : [ "http://nohost/plone/" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "client.secret.creation.time" : "1699476005", + "backchannel.logout.session.required" : "true", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "07f31d32-bd45-409e-8682-d9bf828b9eb4", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "8d314a81-0c9c-479d-83f8-dafc7aea4c76", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/plone/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/plone/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "1397d89c-7b54-42e9-86c2-796ce8a68c5d", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "18d6bc98-f8eb-4c3d-989e-d6b04be053de", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${addressScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "6601ee17-80f3-4e88-8641-31ff34759b6b", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + }, { + "id" : "e50cdeda-a791-46a7-b358-c2feffd217fc", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${rolesScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "3b6131e5-acff-4103-b13d-286fa6d15c5d", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "eff4784c-33cb-49dc-873c-0976b525379b", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "997a02f0-97d8-4a6e-ab68-3ce9154bcff9", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + } ] + }, { + "id" : "f6e28397-9334-495b-99a0-b043fed08113", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "4f61cd08-8f15-45c9-bfc6-07c98fd4ad7a", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false", + "consent.screen.text" : "" + }, + "protocolMappers" : [ { + "id" : "8c553dc5-cf77-4aa6-ae85-c8eb879d249b", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "e14ecf2e-4e63-43ad-9291-633f6999a791", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${emailScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "f79f6646-df39-4fba-9191-44ada9fece30", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "03cb0ed7-1161-4148-82c9-6cc8821c9683", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "6c4c2a32-b75b-4a31-9cd1-b0a80086a14f", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "4315e28e-1a17-4777-88b2-d605efa431bd", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "45d3c251-0599-4127-91e9-ca7801da2254", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${profileScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "f4a5d933-57a5-4c71-b79d-47a04192139c", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "0e28f2cf-5871-47f8-a6c3-a17236e0cfcf", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + }, { + "id" : "4548fc62-309c-4b45-8efa-a51ec1d7f18f", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "7fbb6fd7-08be-434e-bccb-fcbf54acd5dd", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + }, { + "id" : "b6137ba4-e382-42c5-b7b6-7a5eb0d1e399", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "1d39f87b-1aed-4eb9-95a4-33d7dc2881c3", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "12bfce05-c21c-4229-9129-0c227edb7d7f", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "0ef45257-e900-400a-83c3-d60543df4822", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "a96dabbc-21f6-4e80-b26b-917de1a2a22e", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "bd4922fd-ee3a-4eb3-8b9c-4d80c56a5e98", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "d70b6b08-bf8c-4dd1-9335-2721d9ed40b1", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "79bd3718-3774-42d6-af6b-6646fd5f9e88", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "89d21c2e-632d-4d95-8007-d7fd29b375ad", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "b388c60c-721c-409c-ac50-36c24727f79d", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "ea5d4026-9274-47e3-899a-7ae0e8c934f1", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "6f77f324-5036-4602-9ffe-e8772d40b3f0", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + }, { + "id" : "82a6291d-3350-4751-8663-14d78a92facd", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "de9979f8-b935-440c-92e6-4a7a28558e2c", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${phoneScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "77cf9b90-3623-4db0-a016-de4689e322d3", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + }, { + "id" : "e3b1c3f0-eb72-4be9-852a-46e1a23c93c0", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "aa4c4d82-15e6-4e10-8e40-9ed67fec81d3", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "260ddbad-33a4-44d4-af66-2736fbc9639d", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "referrerPolicy" : "no-referrer", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "2e80d970-c9f3-48b8-b057-fba84e65a591", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "e01dd1a8-7e29-40b3-9d54-e77cff78d4fb", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-role-list-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-address-mapper" ] + } + }, { + "id" : "d0a6d964-1346-4995-babe-16039c69f01f", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "b94349fa-b775-497e-8484-9439bed9a8a4", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "be625595-6944-4837-a062-7f3dfe3d3b1c", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "20d7eeb0-1f9c-4779-ab5c-61b10618a783", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "26f3a400-9fbd-4db0-acad-8d93cfaf2589", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "ceadeb6c-7004-48cc-8f31-ae7c7366e40c", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper" ] + } + } ], + "org.keycloak.userprofile.UserProfileProvider" : [ { + "id" : "d38eb268-8a15-44e0-97f9-b2d29b8d2794", + "providerId" : "declarative-user-profile", + "subComponents" : { }, + "config" : { } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "60ef8b28-c012-41ae-9954-b60c0835a7f3", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEpAIBAAKCAQEA4vPCB9uQbzkCENqQ13U7kV/lFH9+c25JK/SRcSuWPqdPQpQU0xrEVwJ48jsfT0+xHDWvRK+mu61gNso4hE/OWcHHv6thmpKoPdQRDQzNvLXkhshTIgnpNiy6CH2pMjK8IPE1hxWXfELN/IXtn65Gf02f0MONcbbV624ylkp1UIevyiYMk62LThMt792djckZlM3FcH5FsmSb9pyp3agdYscwK0XP1t6gijGEf6DMvFe4zW1Fw2lXt0WURFjwC9EtT1A9CHcYdcHUez4Av3qXTYcPzfe80tZguhCw4qFkWnNqAuQh/guLtRiHIJwzO9qc0QtUdg3dfD4LFZQ2KpULVwIDAQABAoIBAARKtoNHYpzI/Gs6vuZTCnD7/p9k09W1a/8/lkOs+h/kxIVOcbvNIZHTYRKUMlxPY9qvgFuDqyQrTmYhtZTm+JnOpfjiBFCT8COPL+Q3yWkX0zUGvF1x7K3k81pAd9Emj8E23olXp0KCY3qysEB9liWQlk3nTNAboh1J3/11LN6hLJKlOu9WR5dW2xp/1ASiv3CEGPQ+xaoaDtyjhMvnS3tjjEaJq3eBAMCkVRSWrDW3o29IR4xG62fVl8XmkyNlkKSg4D1aagQPd4unqBwrkKd77V3hpMf5oH7sCm9mRvvVnjFji0oRq2NFvjwKa8RFImX2HHHz/Jd4WzPngo2nsxUCgYEA9OaEgOFORayS9JIac8A3tkl6dXhskIEMFpkl3JL8yieQca3aM9NgmZgTDvR/A3n9bFkuA45PchbnxGDyKWS2Q7NRKOQbbGXaNRGmgCF6gmPAWqhunKx3AFRMefx0Jj3LLjUD+uzEeJJpnz/5C5J1gvVfAAP/PDQuXS0BjXGROUUCgYEA7Tz+XMtG9QidNpA4lpudQ6LiSWRCWzQWnxmazP92h107F5ZpFeRDj/PqjV+CtC7z6ttR5hIRENVRzznGQc/X/nnhesEef6/Qc5UEEWvMxfucqNJ3pw1tbbXfyj8wbJROsxNgvivbwvzqOx78ZghQeClwyVgxP7TH6KKnMN6spesCgYALHuylbZXAVDXYJUpON0xQWsXUz8MeTL3fyNSPT51rEKDeiKZ/8/lPADjdVPzvi3qWkPYl8zBoXeHoaP6WIpykNmBZGwKjyXw/LK4C44JMqXryovNi6fH9cdaHM4+SrPus/XtQya+iSgPB14XXoK71Y28M8SGF+ligooqPgbi2gQKBgQC+RCm4oGuDP+pehWJtH9cMFMmplNhHL0ppRW7KtKQKQnKPI1mabhzlifl2csBv5CxKEqXngFvhhUR8j1h7FcekV3nIKrRcMdRbroq5shxkOLzXBR1jhDhcQf5+xRkALB3GL2dqfeBdzNvKmLpS1p47EPlc8fdloYanBDRU1HHhCwKBgQC+OPI/5L+o214dAtcha4uuRWPuuNrJkN/IQRu4yN3BCGPuKtmH+B2ev8rB795J81j2eAcHoXBjin3r4kkhtKsqmfXC3bgeu15BXzIu9eVxIURg4kwbYrluxyjA7OvqVk14CNU4mdweeReQhgVSDVsA7OaGl1EwntjT3TFsP+J4mg==" ], + "certificate" : [ "MIICmTCCAYECBgGLsL8JwDANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVwbG9uZTAeFw0yMzExMDgyMTAxMzFaFw0zMzExMDgyMTAzMTFaMBAxDjAMBgNVBAMMBXBsb25lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4vPCB9uQbzkCENqQ13U7kV/lFH9+c25JK/SRcSuWPqdPQpQU0xrEVwJ48jsfT0+xHDWvRK+mu61gNso4hE/OWcHHv6thmpKoPdQRDQzNvLXkhshTIgnpNiy6CH2pMjK8IPE1hxWXfELN/IXtn65Gf02f0MONcbbV624ylkp1UIevyiYMk62LThMt792djckZlM3FcH5FsmSb9pyp3agdYscwK0XP1t6gijGEf6DMvFe4zW1Fw2lXt0WURFjwC9EtT1A9CHcYdcHUez4Av3qXTYcPzfe80tZguhCw4qFkWnNqAuQh/guLtRiHIJwzO9qc0QtUdg3dfD4LFZQ2KpULVwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB5grrNMiL6bj1K4kbp2dFOQUp9ITHlG/J80SKGpiwKGgnJueyfWFc5b3Mnlv/7rBXSfwwlBokyXYvVorwIp4/3P/+1GVv77e7+vZH6ngDXpw8V+jS3KAq7Fvb6cxSkbAOxYtQpuUSox9aKFy87MlMn0QkVs1WOtW7wyRathp2dIKk7V1y2/LeRaVkZhqB3P79vR6LXpkzV2g0jyXh4nqAJh3A9a5AkY5sF4XVRHAL2K+x7fLGk34yc3XpUF1tAtHIbSU9UftuFHkmlmRxwRueWlMbr1A43TDihWZf0mwyi3Lae9XGBRAjKCwJFLl5L91X6C/yk/L4udV6rlwRN4Rw+" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } + }, { + "id" : "646da410-5016-4b15-9c28-8ab23f31dad0", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "041cf696-8d1f-49ba-ad19-fccf110263f6" ], + "secret" : [ "o3xAs7mFqgfG8k_jzB2C9CNfc7x66cK3zIv04pXWHV7_Yeisx9vyDCTCgxozFFtGvvUtCC1KsPfQVLQTeUj4rw" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + }, { + "id" : "1d7cc0af-9f12-477a-b42a-476ec75a9ed7", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEpAIBAAKCAQEAygVjFquZa6NTY8UgyjB3304QOFumStE9LCe2c8wjJ6GDaeMT5CtsmzN+XuclmR2TIEjZoTLeUbjBMLX/vPkgxv5e9t7ObvKyhSeHUZzWNzfnCpt9V3Eruj1m5w5tuhMrxQIqZ/zM0i7X3UKF+9NXMio42oCS2TovR9oiESbGcm3gupP9+h/QEl25Z3fH8pcfvdaSqP5QlRY4VmevzDAqckVtbZwpUaf2GB89KY3XmFTQrMmZ1O5Hwioj8LH1k67jmQsK1/SK1nZpzceLmU7Aq5h4ZHVofTwrHNDdFlwkQtDmbCN1zbBJJMcHJMh5JBS83q4m9E7HkaMtAytKH6p+dwIDAQABAoIBACSstK/laB0/0cAvhcPe56wc1ln83WQAt0vAS+GawhkMO7ip1i8aaQWF5B+jc9JnqZF3WDbo7q5ETaIjsDEkwvwNKr1DdpNysND2e5znzYXJusJUtvz8WRVe10MD/gyFNXF28q7dGGvhwCN1U7CDTfWX76lPJRmVdAblxUB62xTjpblboKz1d3/6e5/OIM4IpEK9v1iylWomPLz4EJoAEkAeV73U2dfZ5pWWAffQ9bws0xsn/EeY/Xh8Qk+2gMnWwcItNM+A7uf7AgkTBV1aomCO+iT6zBo5OF2Fy2Xd92VKK9SsuIM7VgCJUuEIpnVLRqkOAoeVLryQE30v/AR84+0CgYEA+Th9WbUB5qX99dL3J+ee3AQ4e/dcEkRwHDspJY00vdnfgpkt0r75jvu1BYDmKtICEBXQofdO0AG6V0PNHcj0TYyUipFmNE2+9QDMoYNFJaFQ0bCkPa3JFsvbpYtvn5bE67bz+MC1FEyuHJABYM0PE8jWYTJwI8lYSUj00syydzUCgYEAz4Q2CNFRwhoajUwEPjj2uwH9fmLW7PmsYmuB5dqRmPsmIf7353cGySdwwej+I+8CxyGgfb5UyodTSG6FDzZxTrXIK2sWOzsNarUYTB7skvTR1y5xxO/j8g5F2qZW8nJpZ3KnR/KM/ZzHQrqocbDk5BFBjIJNtE1hRuppZIpNWHsCgYEAxEdgOemcAKqU1Cl97Fi4PbG5RrDvO5fY7/MMGf4XHrrcFIBNnaA47gqBbD8zh1/0rfRwZ09eK+Zget/0SNP+zKC+UE/pPaJJla42e7HmIJfnDe7fu5AGQsT1TXD+zaKxnq9tgGlNdBpcOjSUUUfO6xcO+Gf/979YoAcsgmUi3U0CgYEAh8z1r3+MCYIbriv4+t88V9VNltAH5rzdWusP7uPCNGLm6Yh5ozf5cNomqD9ZURqwrrAK3z8XLhKxJc1tti7ip6e+futteQWEGji41dfG6cTdLfxu53o1qco9XIFOlQraf6BAaI0KR025+Kk+Ymho4feI9fKv9zZMWqQ8pFb9ltECgYBRNm0Nqz/scLF/YPHkWENtsPRMQ05rKqgZIXUp1xAKXh6Iv2hFrskrokSmHcIUh7IeuK/elyuaZCvdusf7XfP+UdW29Cki1AMSdoOsypYY3tyRR0ZXAtCfQwbZnLPlir2k7gdfuAAmMhMqdlwp4D6LKxHWVmlYz+kIRyVrhEJ0FA==" ], + "certificate" : [ "MIICmTCCAYECBgGLsL8J5zANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVwbG9uZTAeFw0yMzExMDgyMTAxMzFaFw0zMzExMDgyMTAzMTFaMBAxDjAMBgNVBAMMBXBsb25lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAygVjFquZa6NTY8UgyjB3304QOFumStE9LCe2c8wjJ6GDaeMT5CtsmzN+XuclmR2TIEjZoTLeUbjBMLX/vPkgxv5e9t7ObvKyhSeHUZzWNzfnCpt9V3Eruj1m5w5tuhMrxQIqZ/zM0i7X3UKF+9NXMio42oCS2TovR9oiESbGcm3gupP9+h/QEl25Z3fH8pcfvdaSqP5QlRY4VmevzDAqckVtbZwpUaf2GB89KY3XmFTQrMmZ1O5Hwioj8LH1k67jmQsK1/SK1nZpzceLmU7Aq5h4ZHVofTwrHNDdFlwkQtDmbCN1zbBJJMcHJMh5JBS83q4m9E7HkaMtAytKH6p+dwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBHpAj9koNUC/J41yNP5A6S60VB3durIF87Ton5NuCIQLL4G8Cs7cITOMSVCIg/uChzuIPHgpfG9NmTbJJzAy5nxlUa370ItyuVgwdaDseHg66tHMNqTuF/z0HLGegKcfU+HBhDBgQK9EHsWB7dPB2SwZNJFcF0DYyXZnyQuVhyE9flzPpZe1HY5+J/H0Y8YfzX2Zlu35NZ+zu12u6qDqMnLPVVbZG73YzOu00PreZAtNDVqZngjTOF2eu69D10Wo/vwaJ818vKLlRRZj36YlzW+lNKNRp09m+7rN+7ASkfgg9G03h7btXSrk1suQsLh+UOtqpZnVQN3GDK7l6XOiPX" ], + "priority" : [ "100" ] + } + }, { + "id" : "5cb29139-2aa2-401d-86b3-fde64f347fdc", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "3ac8161e-52a7-4c4a-8ae2-b25f4e5d298f" ], + "secret" : [ "yI9Z3sg6eYL9mxNqBTINzg" ], + "priority" : [ "100" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "1c151c6e-9f2c-407c-8c45-5d9d4f6d4a41", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "beda9d75-6836-4242-bbab-3b49b9cec988", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "dfdc011d-915e-498f-93cb-c46f2bca4946", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "00d326d0-7db8-457a-a500-67108bf205fe", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "50d2093c-4401-433a-848d-7935d7407815", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "13534ca1-09c0-44ab-9b70-2aec5ee5abd7", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "3193de39-46f4-4929-9c6a-c23c2c397ebb", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "d0eb8742-4bee-4769-bba3-93e95462d33a", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "ddfe7e00-078c-451b-adf8-2a4db6538ec2", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "c92b9790-5949-414b-986c-126c7fd307c2", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "23b881d2-9835-487a-b85b-e84b048fdd54", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "7d129309-e391-4236-b224-4cc39641df6e", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "e1dab690-8478-4e1a-b9c0-bc139e8e6768", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "db9d3ddd-b9b8-49a9-a2f3-ae5fd1fd616b", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "6f0af7c3-1591-4917-837f-d3280cbe1d65", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "4a183a9a-56f9-4261-9d3e-e21bff95d975", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-profile-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "84b7fae4-0178-45f9-93db-dc0f4709fb1a", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "e5b84c7d-9b10-440b-b50b-beb1f99d0acf", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "75456819-b47f-48de-9ca3-e8be65d0b99e", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "f966e2b8-3c42-4a03-abe8-3c5aad7ef757", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "TERMS_AND_CONDITIONS", + "name" : "Terms and Conditions", + "providerId" : "TERMS_AND_CONDITIONS", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "CONFIGURE_RECOVERY_AUTHN_CODES", + "name" : "Recovery Authentication Codes", + "providerId" : "CONFIGURE_RECOVERY_AUTHN_CODES", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "UPDATE_EMAIL", + "name" : "Update Email", + "providerId" : "UPDATE_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "webauthn-register", + "name" : "Webauthn Register", + "providerId" : "webauthn-register", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "webauthn-register-passwordless", + "name" : "Webauthn Register Passwordless", + "providerId" : "webauthn-register-passwordless", + "enabled" : true, + "defaultAction" : false, + "priority" : 80, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaAuthRequestedUserHint" : "login_hint", + "clientOfflineSessionMaxLifespan" : "0", + "oauth2DevicePollingInterval" : "5", + "clientSessionIdleTimeout" : "0", + "userProfileEnabled" : "false", + "clientOfflineSessionIdleTimeout" : "0", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false", + "cibaExpiresIn" : "120", + "oauth2DeviceCodeLifespan" : "600", + "parRequestUriLifespan" : "60", + "clientSessionMaxLifespan" : "0", + "frontendUrl" : "", + "acr.loa.map" : "{}" + }, + "keycloakVersion" : "22.0.0", + "userManagedAccessAllowed" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } +} diff --git a/tests/plugin/test_plugin.py b/tests/plugin/test_plugin.py new file mode 100644 index 0000000..218a681 --- /dev/null +++ b/tests/plugin/test_plugin.py @@ -0,0 +1,52 @@ +from base64 import b64decode +from oic.oic.message import OpenIDSchema +from pas.plugins.oidc.utils import PLUGIN_ID +from plone import api +from plone.session.tktauth import splitTicket + +import pytest + + +class TestPlugin: + @pytest.fixture(autouse=True) + def _initialize(self, portal, http_request): + self.pas = api.portal.get_tool("acl_users") + self.plugin = getattr(self.pas, PLUGIN_ID) + self.http_request = http_request + self.http_response = http_request.response + + @pytest.mark.parametrize( + "property,expected", + [ + ("create_user", True), + ("create_ticket", True), + ], + ) + def test_plugin_setup(self, property, expected): + plugin = self.plugin + assert plugin.getProperty(property) is expected + + def test_remember_identity(self): + pas = self.pas + plugin = self.plugin + userinfo = OpenIDSchema(sub="bob") + assert pas.getUserById("bob") is None + # Remember identity + plugin.rememberIdentity(userinfo) + assert pas.getUserById("bob") is not None + # Response tests + assert self.http_response.status == 200 + cookie_value = b64decode(self.http_response.cookies["__ac"]["value"]) + assert splitTicket(cookie_value)[1] == "bob" + + def test_challenge(self): + http_request = self.http_request + http_response = self.http_response + plugin = self.plugin + request_url = http_request.URL + assert plugin.challenge(http_request, http_response) is True + + # Check the response. + plugin_url = plugin.absolute_url() + expected_url = f"{plugin_url}/require_login?came_from={request_url}" + assert http_response.headers["location"] == expected_url diff --git a/tests/setup/test_setup_install.py b/tests/setup/test_setup_install.py new file mode 100644 index 0000000..06f2513 --- /dev/null +++ b/tests/setup/test_setup_install.py @@ -0,0 +1,39 @@ +from pas.plugins.oidc import PACKAGE_NAME +from plone import api + +import pytest + + +class TestSetupInstall: + @pytest.fixture(autouse=True) + def _initialize(self, portal): + self.portal = portal + + def test_addon_installed(self, installer): + assert installer.is_product_installed(PACKAGE_NAME) is True + + def test_latest_version(self, profile_last_version): + """Test latest version of default profile.""" + assert profile_last_version(f"{PACKAGE_NAME}:default") == "1001" + + def test_browserlayer(self, browser_layers): + """Test that IPasPluginsOidcLayer is registered.""" + from pas.plugins.oidc.interfaces import IPasPluginsOidcLayer + + assert IPasPluginsOidcLayer in browser_layers + + def test_plugin_added(self): + """Test if plugin is added to acl_users.""" + from pas.plugins.oidc.utils import PLUGIN_ID + + pas = api.portal.get_tool("acl_users") + assert PLUGIN_ID in pas.objectIds() + + def test_plugin_is_oidc(self): + """Test if we have the correct plugin.""" + from pas.plugins.oidc.plugins import OIDCPlugin + from pas.plugins.oidc.utils import PLUGIN_ID + + pas = api.portal.get_tool("acl_users") + plugin = getattr(pas, PLUGIN_ID) + assert isinstance(plugin, OIDCPlugin) diff --git a/tests/setup/test_setup_uninstall.py b/tests/setup/test_setup_uninstall.py new file mode 100644 index 0000000..9413f34 --- /dev/null +++ b/tests/setup/test_setup_uninstall.py @@ -0,0 +1,27 @@ +from pas.plugins.oidc import PACKAGE_NAME +from plone import api + +import pytest + + +class TestSetupUninstall: + @pytest.fixture(autouse=True) + def uninstalled(self, installer): + installer.uninstall_product(PACKAGE_NAME) + + def test_product_uninstalled(self, installer): + """Test if pas.plugins.oidc is cleanly uninstalled.""" + assert installer.is_product_installed(PACKAGE_NAME) is False + + def test_browserlayer(self, browser_layers): + """Test that IPasPluginsOidcLayer is removed.""" + from pas.plugins.oidc.interfaces import IPasPluginsOidcLayer + + assert IPasPluginsOidcLayer not in browser_layers + + def test_plugin_removed(self, portal): + """Test if plugin is removed to acl_users.""" + from pas.plugins.oidc.utils import PLUGIN_ID + + pas = api.portal.get_tool("acl_users") + assert PLUGIN_ID not in pas.objectIds() diff --git a/tox.ini b/tox.ini index c718e5a..3724172 100644 --- a/tox.ini +++ b/tox.ini @@ -1,35 +1,220 @@ +# Generated from: +# https://github.com/plone/meta/tree/master/config/default +# See the inline comments on how to expand/tweak this configuration file [tox] -minversion = 3.18 +# We need 4.4.0 for constrain_package_deps. +min_version = 4.4.0 envlist = -# plone43-py27 -# plone50-py27 -# plone51-py27 -# plone52-py{27,37,38,39} - plone52-py{37,38,39} - plone60-py{37,38,39,310,311} + lint + test + dependencies -[coverage:run] -relative_files = True + +## +# Add extra configuration options in .meta.toml: +# [tox] +# envlist_lines = """ +# my_other_environment +# """ +# config_lines = """ +# my_extra_top_level_tox_configuration_lines +# """ +## [testenv] -# We do not install with pip, but with buildout: -usedevelop = false +skip_install = true +allowlist_externals = + echo + false +# Make sure typos like `tox -e formaat` are caught instead of silently doing nothing. +# See https://github.com/tox-dev/tox/issues/2858. +commands = + echo "Unrecognized environment name {envname}" + false + +## +# Add extra configuration options in .meta.toml: +# [tox] +# testenv_options = """ +# basepython = /usr/bin/python3.8 +# """ +## + +[testenv:init] +description = Prepare environment +skip_install = true +deps = + mxdev +commands = + mxdev -c mx.ini + echo "Initial setup for mxdev" + + +[testenv:format] +description = automatically reformat code +skip_install = true +deps = + pre-commit +commands = + pre-commit run -a pyupgrade + pre-commit run -a isort + pre-commit run -a black + pre-commit run -a zpretty + +[testenv:lint] +description = run linters that will help improve the code style skip_install = true deps = - !plone60: -rrequirements.txt - plone60: -rrequirements-6.0.x.txt -commands_pre = - plone43: {envbindir}/buildout -Nc {toxinidir}/test_plone43.cfg buildout:directory={envdir} buildout:develop={toxinidir} - plone50: {envbindir}/buildout -Nc {toxinidir}/test_plone50.cfg buildout:directory={envdir} buildout:develop={toxinidir} - plone51: {envbindir}/buildout -Nc {toxinidir}/test_plone51.cfg buildout:directory={envdir} buildout:develop={toxinidir} - plone52: {envbindir}/buildout -Nc {toxinidir}/test_plone52.cfg buildout:directory={envdir} buildout:develop={toxinidir} - plone60: {envbindir}/buildout -Nc {toxinidir}/test_plone60.cfg buildout:directory={envdir} buildout:develop={toxinidir} -# changedir={envdir} -commands = - {envbindir}/coverage run {envbindir}/test - {envbindir}/coverage html -# TODO: increase coverage - {envbindir}/coverage report -m --fail-under=50 -# {envbindir}/coverage report -m --fail-under=85 - {envbindir}/coverage json -i + pre-commit +commands = + pre-commit run -a + +[testenv:dependencies] +description = check if the package defines all its dependencies +skip_install = true +deps = + build + z3c.dependencychecker==2.11 +commands = + python -m build --sdist + dependencychecker + +[testenv:dependencies-graph] +description = generate a graph out of the dependencies of the package +skip_install = false +allowlist_externals = + sh +deps = + pipdeptree==2.5.1 + graphviz # optional dependency of pipdeptree +commands = + sh -c 'pipdeptree --exclude setuptools,wheel,pipdeptree,zope.interface,zope.component --graph-output svg > dependencies.svg' + +[testenv:test] +description = run the distribution tests +use_develop = true +skip_install = false +constrain_package_deps = false +set_env = + ROBOT_BROWSER=headlesschrome + +## +# Specify extra test environment variables in .meta.toml: +# [tox] +# test_environment_variables = """ +# PIP_EXTRA_INDEX_URL=https://my-pypi.my-server.com/ +# """ +# +# Set constrain_package_deps .meta.toml: +# [tox] +# constrain_package_deps = false +## +deps = + pytest-plone + pytest + -c constraints-mxdev.txt + + +## +# Specify additional deps in .meta.toml: +# [tox] +# test_deps_additional = """ +# -esources/plonegovbr.portal_base[test] +# """ +# +# Specify a custom constraints file in .meta.toml: +# [tox] +# constraints_file = "https://my-server.com/constraints.txt" +## +commands = + pytest --disable-warnings {posargs} {toxinidir}/tests +extras = + test + + +[testenv:coverage] +description = get a test coverage report +use_develop = true +skip_install = false +constrain_package_deps = false +set_env = + ROBOT_BROWSER=headlesschrome + +## +# Specify extra test environment variables in .meta.toml: +# [tox] +# test_environment_variables = """ +# PIP_EXTRA_INDEX_URL=https://my-pypi.my-server.com/ +# """ +# +# Set constrain_package_deps .meta.toml: +# [tox] +# constrain_package_deps = "false" +## +deps = + pytest-plone + pytest + coverage + -c constraints-mxdev.txt + +commands = + coverage run --source pas.plugins.oidc -m pytest {posargs} --disable-warnings {toxinidir}/tests + coverage report -m --format markdown + coverage xml +extras = + test + + +[testenv:release-check] +description = ensure that the distribution is ready to release +skip_install = true +deps = + twine + build + towncrier + -c constraints-mxdev.txt + +commands = + # fake version to not have to install the package + # we build the change log as news entries might break + # the README that is displayed on PyPI + towncrier build --version=100.0.0 --yes + python -m build --sdist + twine check dist/* + +[testenv:circular] +description = ensure there are no cyclic dependencies +use_develop = true +skip_install = false +set_env = + +## +# Specify extra test environment variables in .meta.toml: +# [tox] +# test_environment_variables = """ +# PIP_EXTRA_INDEX_URL=https://my-pypi.my-server.com/ +# """ +## +allowlist_externals = + sh +deps = + pipdeptree + pipforester + -c constraints-mxdev.txt + +commands = + # Generate the full dependency tree + sh -c 'pipdeptree -j > forest.json' + # Generate a DOT graph with the circular dependencies, if any + pipforester -i forest.json -o forest.dot --cycles + # Report if there are any circular dependencies, i.e. error if there are any + pipforester -i forest.json --check-cycles -o /dev/null + +## +# Add extra configuration options in .meta.toml: +# [tox] +# extra_lines = """ +# _your own configuration lines_ +# """ +##