#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention Keycloak
#
# Like what you see? Join us!
# https://www.univention.com/about-us/careers/vacancies/
#
# Copyright 2022-2023 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.
"""Manage Univention Keycloak app"""

from __future__ import annotations

import json
import sys
from argparse import SUPPRESS, ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
from base64 import b64decode
from collections import namedtuple
from enum import Enum
from os.path import exists
from subprocess import PIPE, Popen
from typing import Dict
from urllib.parse import quote, urljoin

import requests
from defusedxml import ElementTree
from keycloak import URL_ADMIN_COMPONENTS, URL_ADMIN_REALM, KeycloakAdmin
from keycloak.exceptions import KeycloakError, KeycloakGetError, raise_error_from_response
from ldap.dn import explode_rdn


try:
    from univention.appcenter.app import LooseVersion
    from univention.appcenter.app_cache import Apps
    from univention.config_registry import ucr
    from univention.udm import UDM
    from univention.udm.binary_props import Base64Bzip2BinaryProperty
    from univention.udm.modules.settings_data import SettingsDataObject
    IS_UCS = True
except ImportError:
    ucr = {}
    IS_UCS = False

DEFAULT_REALM = "master"
URL_ADMIN_REQUIRED_ACTIONS = URL_ADMIN_REALM + "/authentication/required-actions"
URL_ADMIN_REQUIRED_ACTION_REGISTER = URL_ADMIN_REALM + "/authentication/register-required-action"
DEFAULT_EXTENTIONS = ["password", "ldapmapper", "self-service"]
DEFAULT_USER_STORAGE_PROVIDER_NAME = "ldap-provider"
KERBEROS_KEYTAB_PATH = "/var/lib/univention-appcenter/apps/keycloak/conf/keycloak.keytab"
# default LDAP attribute mapper
#  * sn -> lastName
#  * givenName -> firstName
#  * mailPrimaryAddress -> email
#  * uid -> uid
#  * uid -> username
#  * modifyTimestamp -> modifyTimestamp
#  * createTimestamp -> createTimestamp
#  * displayName -> displayName
#  * entryUUID -> entryUUID


class KeycloakDomainConfig:

    def __init__(
        self,
        binddn: str | None,
        bindpwd: str | None,
    ) -> None:
        if binddn and bindpwd:
            server = ucr["ldap/master"]
            server_port = ucr["ldap/master/port"]
            udm = UDM.credentials(binddn, bindpwd, server=server, port=server_port).version(3)
        else:
            udm = UDM.admin().version(3)
        self.name = "keycloak"
        self.mod = udm.get("settings/data")
        self.position = f"cn=data,cn=univention,{ucr.get('ldap/base')}"
        apps_cache = Apps()
        installed_apps = apps_cache.get_all_locally_installed_apps()
        keycloak_version = [app.version for app in installed_apps if app.id == "keycloak"]
        if not keycloak_version:
            raise Exception("This command needs to be executed on a UCS system where keycloak is installed")
        self.current_version = keycloak_version[0]
        keycloak_apps = apps_cache.get_all_apps_with_id("keycloak")
        self.keycloak_versions = [app.version for app in keycloak_apps]
        self.keycloak_versions.sort(key=LooseVersion)

    def get_obj(self) -> SettingsDataObject:
        obj = self.mod.get(f'cn={self.name},{self.position}')
        return obj

    def save_obj(self, data: dict, obj: SettingsDataObject) -> None:
        raw_value = json.dumps(data).encode("ascii")
        obj.props.data = Base64Bzip2BinaryProperty("data", raw_value=raw_value)
        obj.save()

    def get(self) -> dict:
        obj = self.get_obj()
        return json.loads(obj.props.data.raw)

    def get_domain_config_version(self) -> str:
        data = self.get()
        return data.get("domain_config_version", "0")

    def set(self, val):
        obj = self.get_obj()
        data = self.get()
        key, value = val.split("=", 1)
        data[key] = value
        self.save_obj(data, obj)

    def set_domain_config_version(self, version: str) -> None:
        if version not in self.keycloak_versions:
            raise Exception(f"{version} is not a valid keycloak version")
        obj = self.get_obj()
        data = json.loads(obj.props.data.raw)
        data["domain_config_version"] = version
        self.save_obj(data, obj)

    def set_domain_config_init(self, version: str) -> None:
        if version not in self.keycloak_versions:
            raise Exception(f"{version} is not a valid keycloak version")
        obj = self.get_obj()
        data = json.loads(obj.props.data.raw)
        data["domain_config_init"] = version
        self.save_obj(data, obj)


class UniventionKeycloakAdmin(KeycloakAdmin):

    def __init__(self, opt: Namespace) -> None:
        KeycloakAdmin.__init__(
            self,
            server_url=opt.keycloak_url,
            username=opt.binduser,
            password=opt.bindpwd,
            realm_name=opt.realm,
            user_realm_name=DEFAULT_REALM,
            verify=opt.no_ssl_verify,
        )

    def get_user_storage_provider_id(self, name: str | None = None) -> str:
        """Get the id for user storage provider"""
        if name is None:
            name = DEFAULT_USER_STORAGE_PROVIDER_NAME
        result = self.get_components(query={"name": name, "type": "org.keycloak.storage.UserStorageProvider"})
        if not result or len(result) != 1:
            raise Exception(f"user storage provider {name} not found")
        return result[0]["id"]

    def get_user_storage_ldap_mappers(self, parent_id: str | None = None) -> list:
        """Get all the org.keycloak.storage.ldap.mappers.LDAPStorageMapper objects from a given component"""
        if parent_id is None:
            parent_id = self.get_user_storage_provider_id()
        mappers = self.get_components(query={"parent": parent_id, "type": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper"})
        return mappers

    def get_mapper(self, client_id: str, mapper_type: str) -> list:
        """Get saml-user-attribute-mapper mapper for given client"""
        _id = self.get_client_id(client_id)
        client = self.get_client(_id)
        mappers = [
            x for x in client.get("protocolMappers", [])
            if x["protocolMapper"] == mapper_type
        ]
        return mappers

    def create_mapper(self, client_id: str, payload: dict) -> None:
        """Create mapper for given client"""
        _id = self.get_client_id(client_id)
        try:
            self.add_mapper_to_client(_id, payload)
        except KeycloakGetError as exc:
            # ignore conflict "Protocol mapper exists with same name"
            if exc.response_code != 409:
                raise

    def delete_mapper(self, client_id: str, name: str) -> None:
        """Delete mapper for given client"""
        _id = self.get_client_id(client_id)
        client = self.get_client(_id)
        for mapper in client.get("protocolMappers"):
            if mapper["name"] == name:
                url = f"admin/realms/{quote(self.realm_name, safe='')}/clients/{quote(_id, safe='')}/protocol-mappers/models/{quote(mapper['id'], safe='')}"
                self.raw_delete(url)
                break
        else:
            print(f"No mapper found with name {name}.")


class ExtType(Enum):
    ACTION = 1
    LDAP_MAPPER = 2


Ext = namedtuple("Ext", ["type", "alias", "name"])


class ExtService(object):

    def __init__(self, kc_admin: KeycloakAdmin):
        self._kc_admin = kc_admin

    @classmethod
    def extensions(cls) -> Dict[str, Ext]:
        return {
            "password": Ext(ExtType.ACTION, "UNIVENTION_UPDATE_PASSWORD", "Univention update password"),
            "self-service": Ext(ExtType.ACTION, "UNIVENTION_SELF_SERVICE", "Univention self service checks"),
            "ldapmapper": Ext(ExtType.LDAP_MAPPER, "univention-ldap-mapper", "Univention ldap mapper"),
        }

    def register_required_action(self, realm: str, alias: str, name: str):
        action = self._get_required_action_by_alias(realm, alias)
        if not action:
            self._create_required_action(realm, alias, name)

    def unregister_required_action(self, realm: str, alias: str):
        action = self._get_required_action_by_alias(realm, alias)
        if action:
            self._delete_required_action(realm, alias)

    def register_ldap_mapper(self, realm: str, alias: str, name: str):
        params = {"providerId": alias, "name": name}
        mapper = self._kc_admin.get_components(params)
        if not mapper:
            params = {"providerId": "ldap-provider", "name": "ldap-provider"}
            parent = self._realm_components(realm, params)
            if not parent:
                raise RuntimeError("ldap-provider with name ldap is not found")

            payload = {
                "name": name,
                "parentId": parent[0]["id"],
                "config": {},
                "providerId": alias,
                "providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
            }
            self._kc_admin.create_component(payload=payload)

    def unregister_ldap_mapper(self, realm: str, alias: str):
        params = {"providerId": alias, "name": "Univention ldap mapper"}
        mapper = self._realm_components(realm, params)
        if mapper:
            self._kc_admin.delete_component(mapper[0]["id"])

    def _realm_components(self, realm: str, query: dict = {}):
        params_path = {"realm-name": realm}
        data_raw = self._kc_admin.raw_get(URL_ADMIN_COMPONENTS.format(**params_path), data=None, **query)

        return raise_error_from_response(data_raw, KeycloakGetError)

    def _create_required_action(self, realm: str, provider_id: str, name: str, enabled: bool = True):
        params = {"realm-name": realm}
        payload = {"providerId": provider_id, "name": name, "enabled": enabled}

        data_raw = self._kc_admin.raw_post(URL_ADMIN_REQUIRED_ACTION_REGISTER.format(**params), data=json.dumps(payload))
        return raise_error_from_response(data_raw, KeycloakError)

    def _get_required_actions(self, realm: str):
        params_path = {"realm-name": realm}
        data_raw = self._kc_admin.raw_get(URL_ADMIN_REQUIRED_ACTIONS.format(**params_path))

        return raise_error_from_response(data_raw, KeycloakGetError)

    def _get_required_action_by_alias(self, realm: str, action_alias: str):
        actions = self._get_required_actions(realm)
        for a in actions:
            if a["alias"] == action_alias:
                return a

        return None

    def _delete_required_action(self, realm: str, alias: str):
        params = {"realm-name": realm}
        url = URL_ADMIN_REQUIRED_ACTIONS.format(**params) + f"/{alias}"

        data_raw = self._kc_admin.raw_delete(url)
        return raise_error_from_response(data_raw, KeycloakGetError)


def check_and_create_component(kc_client, name, prov_id, payload_):
    ldap_component_filter = {'name': name, 'providerId': prov_id}
    ldap_component_list = kc_client.get_components(ldap_component_filter)
    if not ldap_component_list:
        kc_client.create_component(payload=payload_)
        ldap_component_list = kc_client.get_components(ldap_component_filter)

    ldap_component = ldap_component_list.pop()
    return ldap_component.get("id")


def modify_component(kc_client, payload_):
    name = payload_["name"]
    prov_id = payload_['providerId']
    ldap_component_filter = {'name': name, 'providerId': prov_id, 'parentId': payload_['parentId']}
    ldap_component_list = kc_client.get_components(ldap_component_filter)
    if ldap_component_list:
        ldap_component = ldap_component_list.pop()
        payload = ldap_component
        payload["config"].update(payload_["config"])
        kc_client.update_component(ldap_component.get("id"), payload=payload)
        ldap_component_list = kc_client.get_components(ldap_component_filter)

    ldap_component = ldap_component_list.pop()
    return ldap_component.get("id")


def get_realm_id(kc_client, name):
    ldap_realms_list = kc_client.get_realms()
    realm_info = [realm for realm in ldap_realms_list if realm['realm'] == name]
    return realm_info[0]["id"]


def extract_endpoints_xml(metadata_url, metadata_file, no_ssl_verify):
    if not metadata_file:
        xml_content = requests.get(metadata_url, verify=no_ssl_verify).content
    else:
        with open(metadata_file) as fd:
            xml_content = fd.read().strip()
    saml_descriptor_xml = ElementTree.fromstring(xml_content)
    logout_endpoint = saml_descriptor_xml.find('.//{urn:oasis:names:tc:SAML:2.0:metadata}SingleLogoutService').attrib["Location"]
    acs_endpoint = saml_descriptor_xml.find('.//{urn:oasis:names:tc:SAML:2.0:metadata}AssertionConsumerService').attrib["Location"]

    endpoints = {"logout": logout_endpoint, "acs": acs_endpoint}
    return endpoints


def get_SAML_client(opt: Namespace) -> None:
    session = UniventionKeycloakAdmin(opt)
    clients = [client for client in session.get_clients() if client["protocol"] == "saml"]
    if not opt.all:
        clients = [x["clientId"] for x in clients]
    if opt.json:
        print(json.dumps(clients, indent=4))
    else:
        print(clients)


def create_SAML_client(opt):
    print("CREATING KEYCLOAK SAML CLIENT.....")
    if opt.metadata_url:
        client_id = opt.metadata_url
        endpoints = extract_endpoints_xml(client_id, opt.metadata_file, opt.no_ssl_verify)
    elif opt.client_id:
        client_id = opt.client_id
        endpoints = {}
    else:
        raise NotImplementedError()

    # build urls
    single_logout_service_url_post = opt.single_logout_service_url_post or endpoints.get("logout")
    single_logout_service_url_redirect = opt.single_logout_service_url_post or endpoints.get("logout")
    assertion_consumer_url_post = opt.assertion_consumer_url_post or endpoints.get("acs")
    valid_redirect_urls = [assertion_consumer_url_post]

    umc_uid_mapper = [{
        "name": "userid_mapper",
        "protocol": "saml",
        "protocolMapper": "saml-user-attribute-mapper",
        "consentRequired": False,
        "config": {
            "attribute.name": "urn:oid:0.9.2342.19200300.100.1.1",
            "attribute.nameformat": "URI Reference",
            "friendly.name": "uid",
            "user.attribute": "uid",
        },
    }]
    client_payload_saml = {
        "clientId": client_id,
        "surrogateAuthRequired": False,
        "enabled": not opt.not_enabled,
        "alwaysDisplayInConsole": False,
        "clientAuthenticatorType": "client-secret",
        "redirectUris": opt.valid_redirect_uris or valid_redirect_urls,
        "webOrigins": [],
        "description": opt.description,
        "notBefore": 0,
        "bearerOnly": False,
        "consentRequired": False,
        "standardFlowEnabled": True,
        "implicitFlowEnabled": False,
        "directAccessGrantsEnabled": True,
        "serviceAccountsEnabled": False,
        "publicClient": True,
        "frontchannelLogout": not opt.frontchannel_logout_off,
        "protocol": "saml",
        "attributes": {
            "saml_name_id_format": opt.name_id_format,
            "saml.multivalued.roles": "false",
            "saml.force.post.binding": "true",
            "oauth2.device.authorization.grant.enabled": "false",
            "backchannel.logout.revoke.offline.tokens": "false",
            "saml.server.signature.keyinfo.ext": "false",
            "use.refresh.tokens": "true",
            "oidc.ciba.grant.enabled": "false",
            "backchannel.logout.session.required": "true",
            "client_credentials.use_refresh_token": "false",
            "saml.signature.algorithm": "RSA_SHA256",
            "saml.client.signature": "false",
            "require.pushed.authorization.requests": "false",
            "id.token.as.detached.signature": "false",
            "saml.assertion.signature": "true",
            "saml_single_logout_service_url_post": single_logout_service_url_post,
            "saml.encrypt": "false",
            "saml_assertion_consumer_url_post": assertion_consumer_url_post,
            "saml.server.signature": "true",
            "exclude.session.state.from.auth.response": "false",
            "saml.artifact.binding": "false",
            "saml_single_logout_service_url_redirect": single_logout_service_url_redirect,
            "saml_force_name_id_format": "false",
            "tls.client.certificate.bound.access.tokens": "false",
            "acr.loa.map": "{}",
            "saml.authnstatement": "true",
            "display.on.consent.screen": "false",
            "saml.assertion.lifespan": "300",
            "token.response.type.bearer.lower-case": "false",
            "saml.onetimeuse.condition": "false",
            "saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#",
            "policyUri": opt.policy_url,
        },
        "authenticationFlowBindingOverrides": {},
        "fullScopeAllowed": True,
        "nodeReRegistrationTimeout": -1,
        "defaultClientScopes": [
        ],
        "optionalClientScopes": [],
        "access": {
            "view": True,
            "configure": True,
            "manage": True,
        },
    }

    default_uid_mapper = [
        {
            "name": "userid_mapper",
            "protocol": "saml",
            "protocolMapper": "saml-user-attribute-mapper",
            "consentRequired": False,
            "config": {
                "aggregate.attrs": "",
                "attribute.name": "uid",
                "attribute.nameformat": "Basic",
                "friendly.name": "uid",
                "user.attribute": "uid",
            },
        },
    ]

    role_mapper_single = {"config": {
        "attribute.name": "Role",
        "attribute.nameformat": "Basic",
        "friendly.name": "role list mapper",
        "single": "true",
    },
        "name": "role_list_mapper",
        "protocol": "saml",
        "protocolMapper": "saml-role-list-mapper",
    }

    if opt.umc_uid_mapper:
        client_payload_saml["protocolMappers"] = umc_uid_mapper
    else:
        client_payload_saml["protocolMappers"] = default_uid_mapper

    if opt.idp_initiated_sso_url_name:
        client_payload_saml["attributes"]["saml_idp_initiated_sso_url_name"] = opt.idp_initiated_sso_url_name

    if opt.command == "saml/sp" and opt.role_mapping_single_value:
        client_payload_saml["protocolMappers"].append(role_mapper_single)
    if opt.command == "saml/sp" and opt.client_signature_required:
        client_payload_saml["attributes"]["saml.client.signature"] = "true"

    # log into default realm in case UCS realm doesn't exist yet
    kc_admin = KeycloakAdmin(server_url=opt.keycloak_url, username=opt.binduser, password=opt.bindpwd, realm_name=opt.realm, user_realm_name=DEFAULT_REALM, verify=opt.no_ssl_verify)

    kc_admin.create_client(payload=client_payload_saml, skip_exists=True)


def create_oidc_client(opt):
    print("CREATING KEYCLOAK OIDC CLIENT.....")

    # build urls
    server_url = f"https://{opt.host_fqdn}"
    valid_redirect_urls = [opt.app_url, server_url]

    client_payload_oidc = {
        "clientId": opt.client_id,
        "rootUrl": opt.app_url,
        "adminUrl": "",
        "baseUrl": opt.app_url,
        "surrogateAuthRequired": False,
        "enabled": True,
        "alwaysDisplayInConsole": False,
        "clientAuthenticatorType": "client-secret",
        "redirectUris": valid_redirect_urls,
        "webOrigins": valid_redirect_urls,
        "notBefore": 0,
        "bearerOnly": False,
        "consentRequired": False,
        "standardFlowEnabled": True,
        "implicitFlowEnabled": False,
        "directAccessGrantsEnabled": opt.direct_access_grants,
        "serviceAccountsEnabled": False,
        "publicClient": False,
        "frontchannelLogout": True,
        "protocol": "openid-connect",
        "attributes": {
            "saml.multivalued.roles": "false",
            "saml.force.post.binding": "false",
            "frontchannel.logout.session.required": "false",
            "oauth2.device.authorization.grant.enabled": "false",
            "backchannel.logout.revoke.offline.tokens": "false",
            "saml.server.signature.keyinfo.ext": "false",
            "use.refresh.tokens": "true",
            "oidc.ciba.grant.enabled": "false",
            "backchannel.logout.session.required": "false",
            "client_credentials.use_refresh_token": "false",
            "saml.client.signature": "false",
            "require.pushed.authorization.requests": "false",
            "saml.allow.ecp.flow": "false",
            "saml.assertion.signature": "false",
            "id.token.as.detached.signature": "false",
            "client.secret.creation.time": "1661514856",
            "saml.encrypt": "false",
            "saml.server.signature": "false",
            "exclude.session.state.from.auth.response": "false",
            "saml.artifact.binding": "false",
            "saml_force_name_id_format": "false",
            "tls.client.certificate.bound.access.tokens": "false",
            "acr.loa.map": "{}",
            "saml.authnstatement": "false",
            "display.on.consent.screen": "false",
            "token.response.type.bearer.lower-case": "false",
            "saml.onetimeuse.condition": "false",
        },
        "authenticationFlowBindingOverrides": {},
        "fullScopeAllowed": True,
        "nodeReRegistrationTimeout": -1,
        "protocolMappers": [
            {
                "name": "uid",
                "protocol": "openid-connect",
                "protocolMapper": "oidc-usermodel-attribute-mapper",
                "consentRequired": False,
                "config": {
                    "userinfo.token.claim": "true",
                    "user.attribute": "uid",
                    "id.token.claim": "true",
                    "access.token.claim": "true",
                    "claim.name": "uid",
                    "jsonType.label": "String",
                },
            },
            {
                "name": "username",
                "protocol": "openid-connect",
                "protocolMapper": "oidc-usermodel-property-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",
                },
            },
            {
                "name": "email",
                "protocol": "openid-connect",
                "protocolMapper": "oidc-usermodel-property-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",
                },
            },
        ],
        "defaultClientScopes": [
            "web-origins",
            "acr",
            "profile",
            "roles",
            "email",
        ],
        "optionalClientScopes": [
            "address",
            "phone",
            "offline_access",
            "microprofile-jwt",
        ],
        "access": {
            "view": True,
            "configure": True,
            "manage": True,
        },
        "authorizationServicesEnabled": "",
    }

    if opt.client_secret:
        client_payload_oidc["secret"] = opt.client_secret

    # log into default realm in case UCS realm doesn't exist yet
    kc_admin = KeycloakAdmin(server_url=opt.keycloak_url, username=opt.binduser, password=opt.bindpwd, realm_name=opt.realm, user_realm_name=DEFAULT_REALM, verify=opt.no_ssl_verify)
    kc_admin.create_client(payload=client_payload_oidc, skip_exists=True)


def modify_client_scope_mapper(opt):
    # log into default realm in case UCS realm doesn't exist yet
    kc_admin = KeycloakAdmin(server_url=opt.keycloak_url, username=opt.binduser, password=opt.bindpwd, realm_name=opt.realm, user_realm_name=DEFAULT_REALM, verify=opt.no_ssl_verify)

    scopes = kc_admin.get_client_scopes()
    id_role_list = [scope["id"] for scope in scopes if scope["name"] == "role_list"][0]

    scope_id = id_role_list
    data_raw = kc_admin.raw_get(
        f"admin/realms/{opt.realm}/client-scopes/{scope_id}/protocol-mappers/models",
    )
    mappers = data_raw.json()

    mapper_test = [mapper for mapper in mappers if mapper["name"] == "role list"][0]
    id_mapper_role_list = [mapper["id"] for mapper in mappers if mapper["name"] == "role list"][0]

    mapper_test["config"]["single"] = True

    protocol_mapper_id = id_mapper_role_list
    data_raw = kc_admin.raw_put(
        f"admin/realms/{opt.realm}/client-scopes/{scope_id}/protocol-mappers/models/{protocol_mapper_id}",
        data=json.dumps(mapper_test),
    )


def download_cert_oidc(opt):
    print("Downloading KEYCLOAK OIDC CERT.....")
    if opt.oidc_url:
        oidc_conf_url = opt.oidc_url
    else:
        oidc_conf_url = f"{opt.keycloak_url}realms/ucs/.well-known/openid-configuration"

    oidc_conf = requests.get(oidc_conf_url)
    certs_oidc = oidc_conf.json()["jwks_uri"]
    oidc_cert_json = requests.get(certs_oidc).json()

    cert_list = [key["x5c"][0] for key in oidc_cert_json["keys"] if key["use"] == "sig"]
    cert = cert_list[0] + "\n"

    if opt.as_pem:
        cert_der = b64decode(cert)
        p = Popen(['openssl', 'x509', '-inform', 'DER', '-out', opt.output, '-outform', 'PEM'], stdin=PIPE)
        p.communicate(input=cert_der)
    else:
        with open(opt.output, 'w') as fd:
            fd.write(cert)


def download_cert_saml(opt):
    print("Downloading KEYCLOAK SAML CERT.....")
    if opt.saml_url:
        saml_descriptor_url = opt.saml_url
    else:
        saml_descriptor_url = f"{opt.keycloak_url}realms/ucs/protocol/saml/descriptor"

    saml_descriptor = requests.get(saml_descriptor_url)
    saml_descriptor_xml = ElementTree.fromstring(saml_descriptor.content)

    cert = saml_descriptor_xml.find('.//{http://www.w3.org/2000/09/xmldsig#}X509Certificate').text + "\n"

    if opt.as_pem:
        cert_der = b64decode(cert)
        p = Popen(['openssl', 'x509', '-inform', 'DER', '-out', opt.output, '-outform', 'PEM'], stdin=PIPE)
        p.communicate(input=cert_der)
    else:
        with open(opt.output, 'w') as fd:
            fd.write(cert)


def get_client_secret(opt):
    print("Obtaining secret for client ...")

    kc_admin = KeycloakAdmin(server_url=opt.keycloak_url, username=opt.binduser, password=opt.bindpwd, realm_name=opt.realm, user_realm_name=DEFAULT_REALM, verify=opt.no_ssl_verify)
    kc_admin.realm_name = "ucs"

    print(kc_admin.get_client_secrets(kc_admin.get_client_id(opt.client_name)))


def register_extensions(opt: Namespace):
    available_extensions = ExtService.extensions()
    unknown_extensions = set(opt.names) - set(available_extensions.keys())
    if unknown_extensions:
        raise RuntimeError(f"Unknown extensions: {','.join(unknown_extensions)}")

    ext_to_process = set(opt.names) & set(available_extensions.keys())
    funcs = {
        ExtType.ACTION: ExtService.register_required_action,
        ExtType.LDAP_MAPPER: ExtService.register_ldap_mapper,
    }

    kc_admin = UniventionKeycloakAdmin(opt)
    service = ExtService(kc_admin)
    for ext_name in ext_to_process:
        ext = available_extensions.get(ext_name)
        func = funcs.get(ext.type)
        if not func:
            raise RuntimeError(f"No register function found for {ext.type}")
        func(service, opt.realm, ext.alias, ext.name)


def unregister_extensions(opt: Namespace):
    available_extensions = ExtService.extensions()
    unknown_extensions = set(opt.names) - set(available_extensions.keys())
    if unknown_extensions:
        raise RuntimeError(f"Unknown extensions: {','.join(unknown_extensions)}")

    ext_to_process = set(opt.names) & set(available_extensions.keys())
    funcs = {
        ExtType.ACTION: ExtService.unregister_required_action,
        ExtType.LDAP_MAPPER: ExtService.unregister_ldap_mapper,
    }

    kc_admin = UniventionKeycloakAdmin(opt)
    service = ExtService(kc_admin)
    for ext_name in ext_to_process:
        ext = available_extensions.get(ext_name)
        func = funcs.get(ext.type)
        if not func:
            raise RuntimeError(f"No unregister function found for {ext.type}")
        func(service, opt.realm, ext.alias)


def parse_args(args: list[str] | None = None) -> Namespace:
    """
    Parse command line arguments.

    :param args: the list of arguments to process (default: `sys.argv[1:]`)
    :returns: a Namespace instance.
    """
    ldap_base = ucr.get("ldap/base")
    domainname = ucr.get("domainname")
    host_fqdn = "%s.%s" % (ucr.get("hostname"), ucr.get("domainname"))
    umc_saml_sp_server = ucr.get("umc/saml/sp-server", host_fqdn)
    keycloak_fqdn = ucr.get("keycloak/server/sso/fqdn", f"ucs-sso-ng.{domainname}")
    keycloak_url = f"https://{keycloak_fqdn}"
    keycloak_sso_path = ucr.get("keycloak/server/sso/path")
    kerberos_realm = ucr.get("kerberos/realm")

    keycloak_url = urljoin(keycloak_url, keycloak_sso_path)
    if not keycloak_url.endswith("/"):
        keycloak_url = f"{keycloak_url}/"

    no_ucr_available = not (ucr and ldap_base)

    parser = ArgumentParser(description=__doc__)
    parser.add_argument("--binddn", default="")
    parser.add_argument("--binduser", default="admin")
    parser.add_argument("--bindpwd", default="")
    parser.add_argument("--bindpwdfile", default="/etc/keycloak.secret")
    parser.add_argument("--realm", default="ucs")
    parser.add_argument("--keycloak-pwd", default="")
    parser.add_argument("--keycloak-url", required=no_ucr_available, default=keycloak_url)
    parser.add_argument("--no-ssl-verify", action='store_false')
    subparsers = parser.add_subparsers(title="subcommands", description="valid subcommands", required=True, dest="command")

    # saml client
    parser_saml = subparsers.add_parser("saml/sp", help="configure a SAML SP")
    operation_subparsers = parser_saml.add_subparsers(title="operation", description="valid subcommands", required=True, dest="operation")
    create_saml_parser = operation_subparsers.add_parser("create", help="create a new SAML SP in Keycloak")
    create_saml_parser.add_argument("--client-signature-required", action="store_true")
    create_saml_parser_subparser = create_saml_parser.add_mutually_exclusive_group(required=True)
    create_saml_parser_subparser.add_argument("--metadata-url", help="download metadata xml from this url (to extract endpoints) and use as clientId (issuer)")
    create_saml_parser_subparser.add_argument("--client-id", help="clientId (issuer) for this SAML client)")
    create_saml_parser.add_argument("--metadata-file")
    create_saml_parser.add_argument("--umc-uid-mapper", action="store_true")
    create_saml_parser.add_argument("--role-mapping-single-value", action="store_true")
    create_saml_parser.add_argument("--assertion-consumer-url-post", default=None)
    create_saml_parser.add_argument("--single-logout-service-url-post", default=None)
    create_saml_parser.add_argument("--idp-initiated-sso-url-name", default=None)
    create_saml_parser.add_argument("--name-id-format", default="transient")
    create_saml_parser.add_argument("--frontchannel-logout-off", default=False, action="store_true", help="Switch off frontchannelLogout")
    create_saml_parser.add_argument("--description", help="Description of the client")
    create_saml_parser.add_argument("--policy-url", help="URL that the client provides to the end-user to read about the how the profile data will be used")
    create_saml_parser.add_argument("--not-enabled", default=False, action="store_true", help="Disable client")
    create_saml_parser.add_argument("--valid-redirect-uris", nargs='+', default=[], help="Provide list of valid redirect URIs")

    create_saml_parser.set_defaults(func=create_SAML_client)
    get_saml_parser = operation_subparsers.add_parser("get", help="get SAML SP from Keycloak")
    get_saml_parser.add_argument("--json", help="Print json output", default=False, action="store_true")
    get_saml_parser.add_argument("--all", help="Get all client attributes, not just the clientId", default=False, action="store_true")
    get_saml_parser.set_defaults(func=get_SAML_client)

    # saml user attribute mapper
    parser_saml_user_attribute_mapper = subparsers.add_parser("saml-client-user-attribute-mapper", help="Create/get/delete saml-client-user-attribute-mapper for saml clients")
    parser_saml_user_attribute_mapper_subparser = parser_saml_user_attribute_mapper.add_subparsers(title="operation", description="Valid subcommands", required=True, dest="operation")
    parser_create_saml_user_attribute_mapper = parser_saml_user_attribute_mapper_subparser.add_parser("create", help="Create a new saml-client-user-attribute-mapper")
    parser_create_saml_user_attribute_mapper.add_argument("clientid", help="Client ID of the saml client to create the mapper for")
    parser_create_saml_user_attribute_mapper.add_argument("name", help="name of the mapper")
    parser_create_saml_user_attribute_mapper.add_argument("--attribute-nameformat", help="SAML Attribute NameFormat", choices=["URI Reference", "Basic", "Unspecified"], default="Basic")
    parser_create_saml_user_attribute_mapper.add_argument("--user-attribute", help="Name of keycloak user attribute")
    parser_create_saml_user_attribute_mapper.add_argument("--aggregate-attrs", help="Indicates if attribute values should be aggregated with the group attributes", default=False, action="store_true")
    parser_create_saml_user_attribute_mapper.add_argument("--friendly-name", help="An optional, more human-readable form of the attribute's name that can be provided if the actual attribute name is cryptic.")
    parser_create_saml_user_attribute_mapper.add_argument("--attribute-name", help="SAML Attribute Name")
    parser_create_saml_user_attribute_mapper.set_defaults(func=create_saml_user_attribute_mapper)
    parser_delete_saml_user_attribute_mapper = parser_saml_user_attribute_mapper_subparser.add_parser("delete", help="Delete a saml-client-user-attribute-mapper")
    parser_delete_saml_user_attribute_mapper.add_argument("clientid", help="Client ID of the saml client")
    parser_delete_saml_user_attribute_mapper.add_argument("mappername", help="Name of the mapper to delete")
    parser_delete_saml_user_attribute_mapper.set_defaults(func=delete_saml_user_attribute_mapper)
    parser_get_saml_user_attribute_mapper = parser_saml_user_attribute_mapper_subparser.add_parser("get", help="Get the saml-client-user-attribute-mapper for a specific saml client")
    parser_get_saml_user_attribute_mapper.add_argument("clientid", help="Client ID of the saml client")
    parser_get_saml_user_attribute_mapper.add_argument("--json", help="Print json output", default=False, action="store_true")
    parser_get_saml_user_attribute_mapper.add_argument("--all", help="Get all mapper attributes, not just the name", default=False, action="store_true")
    parser_get_saml_user_attribute_mapper.set_defaults(func=get_saml_user_attribute_mapper)

    # saml nameid mapper
    parser_saml_nameid_mapper = subparsers.add_parser("saml-client-nameid-mapper", help="Create/get/delete namid mapper for saml clients")
    parser_saml_nameid_mapper_subparser = parser_saml_nameid_mapper.add_subparsers(title="operation", description="Valid subcommands", required=True, dest="operation")
    parser_create_saml_nameid_mapper = parser_saml_nameid_mapper_subparser.add_parser("create", help="Create a new saml nameid mapper")
    parser_create_saml_nameid_mapper.add_argument("clientid", help="Client ID of the saml client to create the mapper for")
    parser_create_saml_nameid_mapper.add_argument("name", help="name of the mapper")
    parser_create_saml_nameid_mapper.add_argument("--user-attribute", help="Name of keycloak user attribute")
    parser_create_saml_nameid_mapper.add_argument(
        "--mapper-nameid-format",
        help="Name ID Format",
        default="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
        choices=[
            "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
            "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
            "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName",
            "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName",
            "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos",
            "urn:oasis:names:tc:SAML:2.0:nameid-format:entity",
            "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
            "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
        ],
    )
    parser_create_saml_nameid_mapper.add_argument("--base64", help="return the base64 encoded value of the attribut in the mapper", default=False, action="store_true")
    parser_create_saml_nameid_mapper.set_defaults(func=create_saml_nameid_mapper)
    parser_delete_saml_nameid_mapper = parser_saml_nameid_mapper_subparser.add_parser("delete", help="Delete a saml-client-user-attribute-mapper")
    parser_delete_saml_nameid_mapper.add_argument("clientid", help="Client ID of the saml client")
    parser_delete_saml_nameid_mapper.add_argument("mappername", help="Name of the mapper to delete")
    parser_delete_saml_nameid_mapper.set_defaults(func=delete_saml_nameid_mapper)
    parser_get_saml_nameid_mapper = parser_saml_nameid_mapper_subparser.add_parser("get", help="Get the saml-client-user-attribute-mapper for a specific saml client")
    parser_get_saml_nameid_mapper.add_argument("clientid", help="Client ID of the saml client")
    parser_get_saml_nameid_mapper.add_argument("--json", help="Print json output", default=False, action="store_true")
    parser_get_saml_nameid_mapper.add_argument("--all", help="Get all mapper attributes, not just the name", default=False, action="store_true")
    parser_get_saml_nameid_mapper.set_defaults(func=get_saml_nameid_mapper)

    # oidc client
    parser_oidc = subparsers.add_parser("oidc/rp", help="configure a OIDC RP")
    operation_subparsers = parser_oidc.add_subparsers(title="operation", description="valid subcommands", required=True, dest="operation")
    create_oidc_parser = operation_subparsers.add_parser("create", help="create a new OIDC client in Keycloak")
    create_oidc_parser.add_argument("client_id", metavar='CLIENT_ID')
    create_oidc_parser.add_argument("--client-secret", default="")
    create_oidc_parser.add_argument("--app-url", required=True)
    create_oidc_parser.add_argument("--host-fqdn", required=no_ucr_available, default=host_fqdn)
    create_oidc_parser.add_argument("--direct-access-grants", help='enable "Direct access grants" flow (default: False)', default=False, action="store_true")
    create_oidc_parser.set_defaults(func=create_oidc_client)

    create_saml_parser = operation_subparsers.add_parser("secret", help="get client secret from Keycloak")
    create_saml_parser.add_argument("--client-name", required=True)
    create_saml_parser.set_defaults(func=get_client_secret)

    parser_saml_idp_cert = subparsers.add_parser("saml/idp/cert", help="SAML IdP certificate")
    operation_subparsers = parser_saml_idp_cert.add_subparsers(title="operation", description="valid subcommands", required=True, dest="operation")
    get_cert_saml_parser = operation_subparsers.add_parser("get", help="download the SAML IdP signing certificate")
    get_cert_saml_parser.add_argument("--as-pem", action='store_true')
    get_cert_saml_parser.add_argument("--saml-url", default="")
    get_cert_saml_parser.add_argument("--output", required=True)
    get_cert_saml_parser.set_defaults(func=download_cert_saml)

    parser_oidc_op_cert = subparsers.add_parser("oidc/op/cert", help="OIDC provider certificate")
    operation_subparsers = parser_oidc_op_cert.add_subparsers(title="operation", description="valid subcommands", required=True, dest="operation")
    get_cert_oidc_parser = operation_subparsers.add_parser("get", help="download the OIDC signing certificate")
    get_cert_oidc_parser.add_argument("--as-pem", action='store_true')
    get_cert_oidc_parser.add_argument("--oidc-url", default="")
    get_cert_oidc_parser.add_argument("--output", required=True)
    get_cert_oidc_parser.set_defaults(func=download_cert_oidc)

    init_parser = subparsers.add_parser("init", help="configure a Keycloak app")
    init_parser.add_argument("--reverse-proxy-url", default="")
    init_parser.add_argument("--ldap", default="")
    init_parser.add_argument("--ldap-base", required=no_ucr_available, default=ldap_base)
    init_parser.add_argument("--ldap-system-user-dn", required=no_ucr_available, default=f"uid=sys-idp-user,cn=users,{ldap_base}")
    init_parser.add_argument("--ldap-system-user-pwdfile", default="/etc/idp-ldap-user.secret")
    init_parser.add_argument("--host-fqdn", required=no_ucr_available, default=host_fqdn)
    init_parser.add_argument("--umc-saml-sp-server", metavar="FQDN", required=no_ucr_available, default=umc_saml_sp_server)
    init_parser.add_argument("--domainname", required=no_ucr_available, default=domainname)
    init_parser.add_argument("--locales", required=no_ucr_available, default=ucr.get("locale"))
    init_parser.add_argument("--default-locale", required=no_ucr_available, default=ucr.get("locale/default"))
    init_parser.add_argument("--check-init-done", help="only check if init has been already executed", default=False, action="store_true")
    init_parser.set_defaults(func=init_keycloak_ucs)

    kerberos_config = subparsers.add_parser("kerberos-config", help="LDAP federation Kerberos configuration")
    operation_subparsers = kerberos_config.add_subparsers(title="operation", description="valid subcommands", required=True, dest="operation")
    kerberos_config_subparser = operation_subparsers.add_parser("set", help="Set Kerberos configuration")
    kerberos_config_subparser.add_argument("--server-principal", required=True)
    kerberos_config_subparser.set_defaults(func=set_kerberos_spn)

    adhoc_parser = subparsers.add_parser("ad-hoc", help="configure the ad-hoc federation login flow")
    operation_subparsers = adhoc_parser.add_subparsers(title="operation", description="valid subcommands", required=True, dest="operation")
    adhoc_parser_opp = operation_subparsers.add_parser("enable", help="create a login flow that uses ad-hoc federation")
    adhoc_parser_opp.add_argument("--udm-user", required=True)
    adhoc_parser_opp.add_argument("--udm-pwd", required=True)
    adhoc_parser_opp.add_argument("--host-fqdn", required=no_ucr_available, default=host_fqdn)
    adhoc_parser_opp.set_defaults(func=create_adhoc_flow)
    adhoc_parser_opp = operation_subparsers.add_parser("create", help="create and configure an Identity Provider")
    adhoc_parser_opp.add_argument("--alias", default="ADFS", required=True)
    adhoc_parser_opp.add_argument("--metadata-url", required=True)
    adhoc_parser_opp.add_argument("--keycloak-federation-remote-identifier", required=no_ucr_available, default=ucr.get("keycloak/federation/remote/identifier"))
    adhoc_parser_opp.add_argument("--keycloak-federation-source-identifier", required=no_ucr_available, default=ucr.get("keycloak/federation/source/identifier"))
    adhoc_parser_opp.set_defaults(func=create_adhoc_idp)

    fa_parser = subparsers.add_parser("2fa", help="configure 2FA on a group given by --group-2fa (default: \"2FA group\")")
    operation_subparsers = fa_parser.add_subparsers(title="operation", description="valid subcommands", required=True, dest="operation")
    adhoc_parser_opp = operation_subparsers.add_parser("enable", help="create a login flow that uses 2FA and activate it")
    adhoc_parser_opp.add_argument("--group-2fa", default="2FA group")
    adhoc_parser_opp.add_argument("--ldap-base", required=no_ucr_available, default=ldap_base)
    adhoc_parser_opp.set_defaults(func=enable_2fa)

    parser_ext = subparsers.add_parser("extension", help="manage extensions")
    operation_subparsers = parser_ext.add_subparsers(title="operation", description="valid subcommands", required=True, dest="operation")

    ext_register_parser = operation_subparsers.add_parser("register", help="register extension", formatter_class=ArgumentDefaultsHelpFormatter)
    ext_register_parser.add_argument("--names", metavar="name", nargs="+", default=DEFAULT_EXTENTIONS, help="Names of extensions to be activated")
    ext_register_parser.set_defaults(func=register_extensions)

    ext_unregister_parser = operation_subparsers.add_parser("unregister", help="unregister extension", formatter_class=ArgumentDefaultsHelpFormatter)
    ext_unregister_parser.add_argument("--names", metavar="name", nargs="+", default=DEFAULT_EXTENTIONS, help="Names of extensions to be deactivated")
    ext_unregister_parser.set_defaults(func=unregister_extensions)

    # upgrade-config
    parser_upgrade_config = subparsers.add_parser("upgrade-config", help="upgrade keycloak configuration to currently installed version (UCS only)")
    parser_upgrade_config.add_argument("--json", help="json output", default=False, action="store_true")
    parser_upgrade_config.add_argument("--dry-run", help="do nothing, only print", default=False, action="store_true")
    parser_upgrade_config.add_argument("--get-upgrade-steps", help="just print the upgrade steps", default=False, action="store_true")
    parser_upgrade_config.set_defaults(func=upgrade_config)

    # domain-config
    parser_domain_config = subparsers.add_parser("domain-config", help="get/manage keycloak domain config (UCS only)")
    parser_domain_config.add_argument("--json", help="json output", default=False, action="store_true")
    parser_domain_config.add_argument("--get", help="get domain config", default=False, action="store_true")
    # dangerous, hidden, should only by the app itself
    parser_domain_config.add_argument("--set", help=SUPPRESS)
    # dangerous, hidden, should only be used for tests
    parser_domain_config.add_argument("--set-domain-config-version", help=SUPPRESS)
    # dangerous, hidden, should only be used for tests
    parser_domain_config.add_argument("--set-domain-config-init", help=SUPPRESS)
    parser_domain_config.set_defaults(func=domain_config)

    # user-attribute-ldap-mapper for ldap-provider
    parser_user_attribute_ldap_mapper = subparsers.add_parser("user-attribute-ldap-mapper", help="Create/get/delete user-attribute-ldap-mapper for the ldap-provider")
    parser_user_attribute_ldap_mapper_subparser = parser_user_attribute_ldap_mapper.add_subparsers(title="operation", description="Valid subcommands", required=True, dest="operation")
    parser_create_user_attribute_ldap_mapper = parser_user_attribute_ldap_mapper_subparser.add_parser("create", help="Create a new user-attribute-ldap-mapper")
    parser_create_user_attribute_ldap_mapper.add_argument("attributename", help="Name of the mapper, the LDAP attribute name and the user attribute name in keycloak")
    parser_create_user_attribute_ldap_mapper.set_defaults(func=create_user_attribute_ldap_mapper)
    parser_delete_user_attribute_ldap_mapper = parser_user_attribute_ldap_mapper_subparser.add_parser("delete", help="Delete a new user-attribute-ldap-mapper")
    parser_delete_user_attribute_ldap_mapper.add_argument("attributename", help="Name of the mapper to delete ")
    parser_delete_user_attribute_ldap_mapper.set_defaults(func=delete_user_attribute_ldap_mapper)
    parser_get_user_attribute_ldap_mapper = parser_user_attribute_ldap_mapper_subparser.add_parser("get", help="Get all existing user-attribute-ldap-mapper")
    parser_get_user_attribute_ldap_mapper.add_argument("--json", help="Print json output", default=False, action="store_true")
    parser_get_user_attribute_ldap_mapper.add_argument("--all", help="Get all mapper attributes, not just the name", default=False, action="store_true")
    parser_get_user_attribute_ldap_mapper.add_argument("--user-attributes", help="instead of the name of the mapper, print the internal user attribute name, this value can bes used in saml or oicd mappers", default=False, action="store_true")
    parser_get_user_attribute_ldap_mapper.set_defaults(func=get_user_attribute_ldap_mapper)

    # get keycloak base url
    parser_get_base_url = subparsers.add_parser("get-keycloak-base-url", help="get the base url of keycloak according to the current configuration (keycloak/server/sso/fqdn, keycloak/server/sso/path)")
    parser_get_base_url.add_argument("--json", help="json output", default=False, action="store_true")
    parser_get_base_url.set_defaults(func=get_base_url)

    opt = parser.parse_args(args)
    if opt.binddn:
        opt.binduser = explode_rdn(opt.binddn, 1)[0]
    if not exists(opt.bindpwdfile):
        if not opt.bindpwd:
            parser.error(f"Passwordfile {opt.bindpwdfile} for user {opt.binduser} does not exist.")
    else:
        with open(opt.bindpwdfile) as fd:
            opt.bindpwd = fd.read().strip()
    if opt.command == "init":
        if not exists(opt.ldap_system_user_pwdfile):
            if not opt.ldap_system_user_pwdfile:
                parser.error(f"Passwordfile {opt.ldap_system_user_pwdfile} for user {opt.ldap_system_user_dn} does not exist.")
        else:
            with open(opt.ldap_system_user_pwdfile) as fd:
                opt.ldap_system_user_pwd = fd.read().strip()
    opt.kerberos_realm = kerberos_realm
    opt.keycloak_fqdn = keycloak_fqdn
    return opt


def get_base_url(opt: Namespace) -> None:
    """check that the keycloak url works and print url"""
    # check that the url works
    try:
        resp = requests.get(opt.keycloak_url)
        assert resp.status_code == 200, f"invalid http status code {resp.status_code}"
    except (requests.exceptions.ConnectionError, AssertionError) as exc:
        print(exc)
        msg = f"ERROR: Could not connect to keycloak server on {opt.keycloak_url}:\n\n\t{exc}\n\n"
        msg += "Please check the UCR settings for keycloak/server/sso/fqdn and keycloak/server/sso/path,\n"
        msg += "and make sure that keycloak and apache are running on the keycloak server!"
        print(msg, file=sys.stderr)
        return 1
    # remove trailing slash, is this a good idea?
    # the idea is to use this value for
    # "$BASE_URL/realms/ucs/protocol/saml" -> https://ucs-sso-ng.my.domain/realms/ucs/protocol/saml
    url = opt.keycloak_url.strip("/")
    if opt.json:
        print(json.dumps(url, indent=4))
    else:
        print(url)


def delete_saml_nameid_mapper(opt: Namespace) -> None:
    session = UniventionKeycloakAdmin(opt)
    session.delete_mapper(opt.clientid, opt.mappername)


def create_saml_nameid_mapper(opt: Namespace) -> None:
    session = UniventionKeycloakAdmin(opt)
    protocol_mapper = "univention-saml-user-attribute-nameid-mapper-base64" if opt.base64 else "saml-user-attribute-nameid-mapper"
    payload = {
        "name": opt.name,
        "protocol": "saml",
        "protocolMapper": protocol_mapper,
        "config": {
            "user.attribute": opt.user_attribute,
            "mapper.nameid.format": opt.mapper_nameid_format,
        },
    }
    session.create_mapper(opt.clientid, payload)


def get_saml_nameid_mapper(opt: Namespace) -> None:
    session = UniventionKeycloakAdmin(opt)
    mappers = session.get_mapper(opt.clientid, "saml-user-attribute-nameid-mapper")
    mappers += session.get_mapper(opt.clientid, "univention-saml-user-attribute-nameid-mapper-base64")
    if not opt.all:
        mappers = [x["name"] for x in mappers]
    if opt.json:
        print(json.dumps(mappers, indent=4))
    else:
        print(mappers)


def delete_saml_user_attribute_mapper(opt: Namespace) -> None:
    session = UniventionKeycloakAdmin(opt)
    session.delete_mapper(opt.clientid, opt.mappername)


def create_saml_user_attribute_mapper(opt: Namespace) -> None:
    session = UniventionKeycloakAdmin(opt)
    payload = {
        "name": opt.name,
        "protocol": "saml",
        "protocolMapper": "saml-user-attribute-mapper",
        "config": {
            "attribute.nameformat": opt.attribute_nameformat,
            "user.attribute": opt.user_attribute,
            "aggregate.attrs": opt.aggregate_attrs,
            "friendly.name": opt.friendly_name,
            "attribute.name": opt.attribute_name,
        },
    }
    session.create_mapper(opt.clientid, payload)


def get_saml_user_attribute_mapper(opt: Namespace) -> None:
    session = UniventionKeycloakAdmin(opt)
    mappers = session.get_mapper(opt.clientid, "saml-user-attribute-mapper")
    if not opt.all:
        mappers = [x["name"] for x in mappers]
    if opt.json:
        print(json.dumps(mappers, indent=4))
    else:
        print(mappers)


def get_user_attribute_ldap_mapper(opt: Namespace) -> None:
    session = UniventionKeycloakAdmin(opt)
    mappers = session.get_user_storage_ldap_mappers()
    if opt.user_attributes:
        mappers = [
            x.get("config").get("user.model.attribute")[0]
            for x in mappers
            if x.get("config").get("user.model.attribute")
        ]
    elif not opt.all:
        mappers = [x["name"] for x in mappers]
    if opt.json:
        print(json.dumps(mappers, indent=4))
    else:
        print(mappers)


def delete_user_attribute_ldap_mapper(opt: Namespace) -> None:
    session = UniventionKeycloakAdmin(opt)
    for mapper in session.get_user_storage_ldap_mappers():
        if mapper["name"] == opt.attributename:
            session.delete_component(mapper["id"])
            break
    else:
        print(f"LDAP provider mapper {opt.attributename} not found, nothing to delete.")


def set_kerberos_spn(opt: Namespace) -> None:
    session = UniventionKeycloakAdmin(opt)
    name = DEFAULT_USER_STORAGE_PROVIDER_NAME
    provider_id = session.get_user_storage_provider_id()
    payload = {
        "name": name,
        "providerType": "org.keycloak.storage.UserStorageProvider",
        "parentId": get_realm_id(session, opt.realm),
        "providerId": provider_id,
        "config": {
            "serverPrincipal": [getattr(opt, "server_principal", f"HTTP/{opt.keycloak_fqdn}@{opt.kerberos_realm}")],
        },
    }
    modify_component(session, payload)


def create_kerberos_config(opt: Namespace) -> None:
    session = UniventionKeycloakAdmin(opt)
    name = DEFAULT_USER_STORAGE_PROVIDER_NAME
    provider_id = session.get_user_storage_provider_id()
    payload = {
        "name": name,
        "providerType": "org.keycloak.storage.UserStorageProvider",
        "parentId": get_realm_id(session, opt.realm),
        "providerId": provider_id,
        "config": {
            "allowKerberosAuthentication": ["true"],
            "kerberosRealm": [opt.kerberos_realm],
            "serverPrincipal": [f"HTTP/{opt.keycloak_fqdn}@{opt.kerberos_realm}"],
            "keyTab": [KERBEROS_KEYTAB_PATH],
        },
    }
    modify_component(session, payload)


def create_user_attribute_ldap_mapper(opt: Namespace) -> None:
    session = UniventionKeycloakAdmin(opt)

    provider_id = session.get_user_storage_provider_id()
    existing_mappers = session.get_user_storage_ldap_mappers(provider_id)
    if opt.attributename in [x["name"] for x in existing_mappers]:
        print(f"LDAP provider mapper {opt.attributename} already exists.")
    else:
        payload = {
            "name": opt.attributename,
            "parentId": provider_id,
            "providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
            "providerId": "user-attribute-ldap-mapper",
            "config": {
                "ldap.attribute": [opt.attributename],
                "is.mandatory.in.ldap": ["false"],
                "attribute.force.default": ["false"],
                "is.binary.attribute": ["false"],
                "read.only": ["true"],
                "user.model.attribute": [opt.attributename],
            },
        }
        session.create_component(payload)


def domain_config(opt: Namespace) -> None:
    if IS_UCS:
        kdc = KeycloakDomainConfig(opt.binddn, opt.bindpwd)
        if opt.get:
            versions = kdc.get()
            out = json.dumps(versions, indent=4) if opt.json else versions
            print(out)
        if opt.set:
            kdc.set(opt.set)
        if opt.set_domain_config_version:
            kdc.set_domain_config_version(opt.set_domain_config_version)
        if opt.set_domain_config_init:
            kdc.set_domain_config_init(opt.set_domain_config_init)
    else:
        print("Only supported on UCS systems")


def upgrade_config(opt: Namespace) -> None:
    if IS_UCS:
        # these are the version that need a config upgrade
        # never change the order of this list, just append
        upgrades = [
            "19.0.2-ucs2",
            "21.1.1-ucs1",
        ]
        upgrades.sort(key=LooseVersion)
        kdc = KeycloakDomainConfig(opt.binddn, opt.bindpwd)
        domain_config_version = kdc.get_domain_config_version()
        # check what needs to be done
        upgrade_steps = [
            version for version in upgrades
            # only those versions that are greater than the current domain config version
            if LooseVersion(version) > LooseVersion(domain_config_version)
            # only up until the currently installed version
            if LooseVersion(version) <= LooseVersion(kdc.current_version)
        ]
        if opt.get_upgrade_steps:
            out = json.dumps(upgrade_steps, indent=4) if opt.json else upgrade_steps
            print(out)
            return
        # check if we are already up-to-date
        if not upgrade_steps:
            print(f"Nothing to do, already at domain config version {domain_config_version}")
            return
        for version in upgrade_steps:
            if version == "19.0.2-ucs2":
                print(f"Running update steps for version: {version}")
                if not opt.dry_run:
                    opt.names = ['password', 'ldapmapper', 'self-service']
                    register_extensions(opt)
                    kdc.set_domain_config_version(version)
                    kdc.set_domain_config_init(version)  # new in this version, normally only init init_keycloak
            if version == "21.1.1-ucs1":
                print(f"Running update steps for version: {version}")
                if not opt.dry_run:
                    opt.attributename = "displayName"
                    create_user_attribute_ldap_mapper(opt)
                    opt.attributename = "entryUUID"
                    create_user_attribute_ldap_mapper(opt)
                    create_kerberos_config(opt)
                    kdc.set_domain_config_version(version)
    else:
        print("Only supported on UCS systems")


def create_adhoc_idp(opt):
    print("Creating ad hoc federation identity provider...")
    # realm config
    kc_admin = KeycloakAdmin(server_url=opt.keycloak_url, username=opt.binduser, password=opt.bindpwd, realm_name=opt.realm, user_realm_name=DEFAULT_REALM, verify=opt.no_ssl_verify)
    kc_admin.realm_name = "ucs"

    metadata_info = extract_metadata(opt.metadata_url, opt.no_ssl_verify)

    idp_payload = {
        "addReadTokenRoleOnCreate": False,
        "alias": opt.alias,
        "authenticateByDefault": False,
        "config": {
            "addExtensionsElementWithKeyInfo": "false",
            "allowCreate": "true",
            "allowedClockSkew": "",
            "attributeConsumingServiceIndex": "",
            "authnContextComparisonType": "exact",
            "backchannelSupported": "",
            "encryptionPublicKey": metadata_info["encryption"],
            "entityId": "keycloak_ucs",
            "forceAuthn": "false",
            "hideOnLoginPage": "",
            "loginHint": "false",
            "nameIDPolicyFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
            "postBindingAuthnRequest": "true",
            "postBindingLogout": "false",
            "postBindingResponse": "true",
            "principalAttribute": "sAMAccountName",
            "principalType": "ATTRIBUTE",
            "signatureAlgorithm": "RSA_SHA256",
            "signingCertificate": metadata_info["signing"],
            "signSpMetadata": "false",
            "singleSignOnServiceUrl": metadata_info["sso_logout_url"],
            "syncMode": "IMPORT",
            "useJwksUrl": "true",
            "validateSignature": "true",
            "wantAssertionsEncrypted": "false",
            "wantAssertionsSigned": "false",
            "wantAuthnRequestsSigned": "true",
            "xmlSigKeyInfoKeyNameTransformer": "CERT_SUBJECT",
        },
        "displayName": opt.alias,
        "enabled": True,
        "firstBrokerLoginFlowAlias": "Univention-Authenticator ad hoc federation flow",
        "linkOnly": False,
        "postBrokerLoginFlowAlias": "",
        "providerId": "saml",
        "storeToken": False,
        "trustEmail": False,
        "updateProfileFirstLoginMode": "on",
    }
    kc_admin.create_idp(idp_payload)

    mapper_payload = {
        "config": {
            "attribute.friendly.name": "",
            "attribute.name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
            "attributes": "[]",
            "syncMode": "IMPORT",
            "user.attribute": "email",
        },
        "identityProviderAlias": opt.alias,
        "identityProviderMapper": "saml-user-attribute-idp-mapper",
        "name": "email",
    }
    kc_admin.add_mapper_to_idp(opt.alias, mapper_payload)

    mapper_payload = {
        "config": {
            "attribute.friendly.name": "",
            "attribute.name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
            "attributes": "[]",
            "syncMode": "IMPORT",
            "user.attribute": "firstName",
        },
        "identityProviderAlias": opt.alias,
        "identityProviderMapper": "saml-user-attribute-idp-mapper",
        "name": "firstName",
    }
    kc_admin.add_mapper_to_idp(opt.alias, mapper_payload)

    mapper_payload = {
        "config": {
            "attribute.friendly.name": "",
            "attribute.name": "objectGuid",
            "attributes": "[]",
            "syncMode": "IMPORT",
            "user.attribute": opt.keycloak_federation_remote_identifier,
        },
        "identityProviderAlias": opt.alias,
        "identityProviderMapper": "saml-user-attribute-idp-mapper",
        "name": "remoteIdentifier",
    }
    kc_admin.add_mapper_to_idp(opt.alias, mapper_payload)

    mapper_payload = {
        "config": {
            "attribute.friendly.name": "",
            "attribute.name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
            "attributes": "[]",
            "syncMode": "IMPORT",
            "user.attribute": "lastName",
        },
        "identityProviderAlias": opt.alias,
        "identityProviderMapper": "saml-user-attribute-idp-mapper",
        "name": "lastName",
    }
    kc_admin.add_mapper_to_idp(opt.alias, mapper_payload)

    mapper_payload = {
        "config": {
            "attribute": "sourceID",
            "attribute.value": "ADFS",
            "attributes": "[]",
            "syncMode": "INHERIT",
        },
        "identityProviderAlias": opt.alias,
        "identityProviderMapper": "hardcoded-attribute-idp-mapper",
        "name": opt.keycloak_federation_source_identifier,
    }
    kc_admin.add_mapper_to_idp(opt.alias, mapper_payload)

    mapper_payload = {
        "config": {
            "attributes": "[]",
            "syncMode": "IMPORT",
            "target": "LOCAL",
            "template": "external-${ALIAS}-${ATTRIBUTE.sAMAccountName}",
        },
        "identityProviderAlias": opt.alias,
        "identityProviderMapper": "saml-username-idp-mapper",
        "name": "username mapper",
    }
    kc_admin.add_mapper_to_idp(opt.alias, mapper_payload)


def extract_metadata(metadata_url, no_ssl_verify):
    xml_content = requests.get(metadata_url, verify=no_ssl_verify).content
    saml_descriptor_xml = ElementTree.fromstring(xml_content)
    signing_cert = saml_descriptor_xml.find('.//{http://www.w3.org/2000/09/xmldsig#}X509Certificate').text
    encryption_cert = saml_descriptor_xml.findall(".//{http://www.w3.org/2000/09/xmldsig#}X509Certificate")[1].text
    sso_url = saml_descriptor_xml.find('.//{urn:oasis:names:tc:SAML:2.0:metadata}SingleLogoutService').attrib["Location"]

    metadata_info = {"encryption": encryption_cert, "signing": signing_cert, "sso_logout_url": sso_url}
    return metadata_info


def enable_2fa(opt):
    print("Enabling 2FA ...")
    print(f"Using KC_URL: {opt.keycloak_url}")

    # realm config
    kc_admin = KeycloakAdmin(server_url=opt.keycloak_url, username=opt.binduser, password=opt.bindpwd, realm_name=opt.realm, user_realm_name=DEFAULT_REALM, verify=opt.no_ssl_verify)
    kc_admin.realm_name = "ucs"
    realm_2fa_role = "2FA role"
    ldap_2fa_group = opt.group_2fa  # FIXME: Check group to use for 2FA

    ldap_component_filter = {'name': 'ldap-provider', 'providerId': 'ldap'}
    ldap_component_list = kc_admin.get_components(ldap_component_filter)
    provider = ldap_component_list.pop()
    provider_id = provider["id"]
    mapper_id = add_or_replace_ldap_group_mapper(opt, kc_admin, provider, "univention-groups", ldap_2fa_group)

    # Synchronize LDAP groups to Keycloak
    kc_admin.raw_post(f'/admin/realms/ucs/user-storage/{provider_id}/mappers/{mapper_id}/sync?direction=fedToKeycloak', data={})

    create_realm_role(kc_admin, opt.realm, realm_2fa_role)
    create_realm_group(kc_admin, opt.realm, ldap_2fa_group)  # FIXME: This could be removed if we only use UDM groups
    assign_group_realm_role_by_group_name(kc_admin, opt.realm, ldap_2fa_group, realm_2fa_role)

    # switch to default flow before changing anything
    flow_name = "2fa-browser"
    kc_admin.update_realm(opt.realm, {"browserFlow": "browser"})

    # create browser flow
    create_conditional_2fa_flow(kc_admin, opt.realm, realm_2fa_role, flow_name)

    # change authentication binding to conditional 2fa flow
    kc_admin.update_realm(opt.realm, {"browserFlow": flow_name})


def create_or_replace_realm_role(kc_admin, realm, role):
    REALM_ROLE_BASE = {
        "name": None,
        "composite": False,
        "clientRole": False,
        # "containerId": None,
        "attributes": {},
    }

    # check for existing role
    roles = kc_admin.get_realm_roles()
    for iter_role in roles:
        if role == iter_role["name"]:
            kc_admin.delete_realm_role(role)

    # create payload
    payload = REALM_ROLE_BASE
    payload["name"] = role
    #payload["containerId"] = role

    url = f"admin/realms/{realm}/roles"
    data_raw = kc_admin.raw_post(url, data=json.dumps(payload))
    return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204, 201])


def create_realm_role(kc_admin, realm, role):
    REALM_ROLE_BASE = {
        "name": None,
        "composite": False,
        "clientRole": False,
        # "containerId": None,
        "attributes": {},
    }

    # check for existing role
    roles = kc_admin.get_realm_roles()
    for iter_role in roles:
        if role == iter_role["name"]:
            print("Group already exists")  # Avoid deleting the role, it will cause to unbind role from groups already bind
            return

    # create payload
    payload = REALM_ROLE_BASE
    payload["name"] = role

    url = f"admin/realms/{realm}/roles"
    data_raw = kc_admin.raw_post(url, data=json.dumps(payload))
    return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204, 201])


def create_realm_group(kc_admin, realm, group):
    """
    Create or replace if exists a realm group
    :param kc_admin: Keycloak client
    :param realm: String
    :param group: String
    :return: Keycloak server response (RealmRepresentation)
    """
    REALM_GROUP_BASE = {
        "name": None,
    }

    # check for existing group
    groups = kc_admin.get_groups()
    for iter_group in groups:
        if group == iter_group["name"]:
            print("Group already exists")  # Avoid deleting the group, it will cause to unbind ldap users if the group already exists
            return

    # create payload
    payload = REALM_GROUP_BASE
    payload["name"] = group

    return kc_admin.create_group(payload)


def assign_group_realm_role_by_group_name(kc_admin, realm, group_name, role):
    """
    Assign a keycloak realm role to a group by it's name (rather than it's internal id).
    Users in this group will automatically be part of the assinged role.
    :param kc_admin: Keycloak client
    :param realm: String
    :param group_name: String
    :param role: String
    :return: Keycloak server response (RealmRepresentation)
    """
    groups = kc_admin.get_groups()  # query={"name": group_name}
    groups = [group for group in groups if group["name"] == group_name]

    if not groups:
        print("Warning: Groupname does not exist, realm role can not be linked")
        return
    try:
        group_id = [g["id"] for g in groups if g["name"] == group_name][0]
    except IndexError:
        group_id = None  # why is group ID allowed to be none?

    # get role id
    try:
        realm_role_id = [r["id"] for r in kc_admin.get_realm_roles() if r["name"] == role][0]
    except IndexError:
        print(f"WARNING: role {role} does not exist")
        return

    payload = [
        {
            "id": realm_role_id,
            "containerId": realm,
            "clientRole": False,
            "name": role,
        },
    ]

    data_raw = kc_admin.raw_post(f"admin/realms/{realm}/groups/{group_id}/role-mappings/realm", data=json.dumps(payload))
    return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204])


def add_or_replace_ldap_group_mapper(opt, kc_admin, ldap_sp, name, ldap_group):
    """
    Add or replace if exists component mapper of type ldap-attribute-mapper
    :param kc_admin: Keycloak client
    :param ldap_sp: KeycloakREST_LDAPComponentRepresentation
    :param name: String
    :param ldap_group: String
    :return: Keycloak server response (RealmRepresentation)
    """
    LDAP_GROUP_MAPPER = {
        "name": None,
        "parentId": None,
        "providerId": "group-ldap-mapper",
        "providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
        "config": {
            "membership.attribute.type": [
                "UID",
            ],
            "group.name.ldap.attribute": [
                "cn",
            ],
            "preserve.group.inheritance": [
                "false",
            ],
            "membership.user.ldap.attribute": [
                "uid",
            ],
            "groups.dn": [
                None,
            ],
            "mode": [
                "READ_ONLY",
            ],
            "user.roles.retrieve.strategy": [
                "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE",
            ],
            "ignore.missing.groups": [
                "false",
            ],
            "membership.ldap.attribute": [
                "memberUid",
            ],
            "memberof.ldap.attribute": [
                "memberOf",
            ],
            "group.object.classes": [
                "univentionGroup",
            ],
            "groups.path": [
                "/",
            ],
            "drop.non.existing.groups.during.sync": [
                "false",
            ],
        },
    }

    payload = LDAP_GROUP_MAPPER
    payload["parentId"] = ldap_sp["id"]
    payload["name"] = name
    payload["config"]["groups.dn"] = [f"cn=groups,{opt.ldap_base}"]

    add_or_replace_mapper(kc_admin, component=payload)

    mapper_component_filter = {'name': name, 'parentId': payload['parentId']}
    component_list = kc_admin.get_components(mapper_component_filter)
    return component_list[0]["id"]


def add_or_replace_mapper(kc_admin, component):
    """
    Add or replace if exists any component mapper
    :param kc_admin: Keycloak client
    :param component: KeycloakREST_ComponentRepresentation
    :return:  Keycloak server response (RealmRepresentation)
    """
    mapper_component_filter = {'name': component["name"], 'parentId': component['parentId']}
    component_list = kc_admin.get_components(mapper_component_filter)
    for mapper in component_list:
        kc_admin.delete_component(mapper["id"])

    return kc_admin.create_component(payload=component)


def create_conditional_2fa_flow(kc_admin, realm, conditional_role, flow_name):
    """
    Create a conditional 2FA-require authentication flow for a given role
    :param kc_admin: Keycloak client
    :param realm: String
    :param conditional_role: String
    :param flow_name: String
    :return: None
    """
    # delete old flow/subflows
    flows = kc_admin.get_authentication_flows()
    for flow in flows:
        if flow["alias"].startswith(flow_name):
            delete_authentication_flow(kc_admin, flow["id"])
            print("Deleted: ", flow["alias"])

    # copy default browser flow
    kc_admin.copy_authentication_flow(payload=json.dumps({"newName": flow_name}), flow_alias="browser")

    # create role condition
    master_subflow = f"{flow_name} Browser - Conditional OTP"
    payload = {"provider": "conditional-user-role", "requirement": "REQUIRED"}
    kc_admin.create_authentication_flow_execution(payload=json.dumps(payload), flow_alias=master_subflow)

    # determine user role execution id
    condition_user_role_id = None
    for exe in kc_admin.get_authentication_flow_executions(flow_name):
        if exe.get("authenticationFlow"):
            continue
        if exe["providerId"] == "conditional-user-role":
            condition_user_role_id = exe["id"]

    # raise role condition prio (twice)
    execution_raise_priority(kc_admin, condition_user_role_id)
    execution_raise_priority(kc_admin, condition_user_role_id)

    # FIXME: bug in keycloak API means requirement = REQUIRED can not be set at creation
    # if this bug is fixed the following lines may be deleted
    payload_update_required_state = {
        "id": condition_user_role_id,
        "requirement": "REQUIRED",
    }
    try:
        kc_admin.update_authentication_flow_executions(payload=json.dumps(payload_update_required_state), flow_alias=flow_name)
    except KeycloakGetError as exc:
        if exc.response_code != 202:
            raise
    # FIXME end

    # config role condition
    config = {
        "alias": "2fa-role-mapping",
        "config": {
            "condUserRole": conditional_role,
            "negate": "",
        },
    }
    create_config_for_execution(kc_admin, config, condition_user_role_id)

    # disable user configured
    condition_user_configured_id = None
    for exe in kc_admin.get_authentication_flow_executions(flow_name):
        if exe.get("authenticationFlow"):
            continue
        if exe["providerId"] == "conditional-user-configured":
            condition_user_configured_id = exe["id"]

    payload_condition_user = {"id": condition_user_configured_id, "requirement": "DISABLED"}

    # FIXME: keycloak API bug, responds with 202
    try:
        kc_admin.update_authentication_flow_executions(json.dumps(payload_condition_user), flow_name)
    except KeycloakGetError as exc:
        if exc.response_code != 202:
            raise exc


def create_config_for_execution(kc_admin, config, execution_id):
    """
    Create an extended config for an existing flow execution
    :param kc_admin: Keycloak client
    :param config: KeycloakREST_ExecutionConfigRepresentation
    :param execution_id: String
    :return: Keycloak server response (RealmRepresentation)
    """
    url = f"admin/realms/{kc_admin.realm_name}/authentication/executions/{execution_id}/config"
    data_raw = kc_admin.raw_post(url, data=json.dumps(config))
    return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204, 201])


def delete_authentication_flow(kc_admin, flow_id):
    data_raw = kc_admin.raw_delete(f"admin/realms/{kc_admin.realm_name}/authentication/flows/{flow_id}")
    return raise_error_from_response(data_raw, KeycloakError, expected_codes=[204])


def execution_raise_priority(kc_admin, execution_id):
    """
    Raise the priority of a give execution within it's subflow.
    Calling this for the top priority execution has no effect, but will not fail.
    :param kc_admin: Keycloak client
    :param execution_id: String
    :return: Keycloak server response (RealmRepresentation)
    """
    payload = {"realm": kc_admin.realm_name, "execution": execution_id}

    url = f"admin/realms/{kc_admin.realm_name}/authentication/executions/{execution_id}/raise-priority"
    data_raw = kc_admin.raw_post(url, data=json.dumps(payload))
    return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204, 201])


def create_adhoc_flow(opt):
    print("Creating ad hoc federation authentication flow...")

    print(f"Using KC_URL: {opt.keycloak_url}")

    # realm config
    kc_admin = KeycloakAdmin(server_url=opt.keycloak_url, username=opt.binduser, password=opt.bindpwd, realm_name=opt.realm, user_realm_name=DEFAULT_REALM, verify=opt.no_ssl_verify)
    kc_admin.realm_name = "ucs"

    payload_authflow = {
        "newName": "Univention-Authenticator ad hoc federation flow",
    }

    kc_admin.copy_authentication_flow(payload=json.dumps(payload_authflow), flow_alias='first broker login')

    payload_exec_flow = {
        'provider': "univention-authenticator",
    }
    kc_admin.create_authentication_flow_execution(payload=json.dumps(payload_exec_flow), flow_alias='Univention-Authenticator ad hoc federation flow')

    execution_list = kc_admin.get_authentication_flow_executions("Univention-Authenticator ad hoc federation flow")
    ua_execution = [flow for flow in execution_list if flow["displayName"] == "Univention Authenticator"][0]
    payload_exec_flow = {
        "id": ua_execution["id"],
        "requirement": "REQUIRED",
        "displayName": "Univention Authenticator",
        "requirementChoices": [
            "REQUIRED",
            "DISABLED",
        ],
        "configurable": "true",
        "providerId": "univention-authenticator",
        "level": 0,
        "index": 2,
    }
    try:
        kc_admin.update_authentication_flow_executions(payload=json.dumps(payload_exec_flow), flow_alias='Univention-Authenticator ad hoc federation flow')
    except KeycloakError as exc:
        print(exc)
        if exc.response_code != 202:  # FIXME: function expected 204 response it gets 202
            raise

    # config_id from 'authenticationConfig' in get_authentication_flow_executions
    config_ua = {
        "config": {
            "udm_endpoint": f"https://{opt.host_fqdn}/univention/udm",
            "udm_user": opt.UDM_user,
            "udm_password": opt.UDM_pwd,
        },
        "alias": "localhost config",
    }

    #  check raise error
    data_raw = kc_admin.raw_post("admin/realms/{}/authentication/executions/{}/config".format(kc_admin.realm_name, ua_execution["id"]), json.dumps(config_ua))
    print("Response code from config Univention-Authenticator: ", data_raw.status_code)


def init_keycloak_ucs(opt):
    locales = opt.locales.split()
    locales_format = [locale[:locale.index("_")] for locale in locales]
    default_locale = opt.default_locale[:opt.default_locale.index("_")]
    if opt.reverse_proxy_url:
        opt.keycloak_url = opt.reverse_proxy_url

    # user federation
    if opt.ldap:
        ucs_ldap_url = opt.ldap
    else:
        ucs_ldap_url = f"ldap://{opt.host_fqdn}:7389"  # "ldap://{FQDN}:{port}"

    print(f"Using bind-dn: {opt.binddn}")

    # log into default realm in case UCS realm doesn't exist yet
    kc_admin = KeycloakAdmin(server_url=opt.keycloak_url, username=opt.binduser, password=opt.bindpwd, realm_name=opt.realm, user_realm_name=DEFAULT_REALM, verify=opt.no_ssl_verify)

    # check if we need to run init
    print("Check if init is needed", end=": ")
    realm = [
        realm for realm in kc_admin.get_realms()
        if realm.get("realm", "") == opt.realm
    ]
    if realm:
        print("no, already executed")
        sys.exit(0)
    else:
        if opt.check_init_done:
            print("yes, but stopping as requested")
            sys.exit(1)
        else:
            print("yes, continuing init")

    # set locale languages
    realm_payload = {
        "id": DEFAULT_REALM,
        "realm": DEFAULT_REALM,
        "enabled": True,
        "internationalizationEnabled": True,
        "supportedLocales": locales_format,
        "defaultLocale": default_locale,
        "adminTheme": "keycloak",
        "accountTheme": "keycloak",
        "emailTheme": "keycloak",
        "loginTheme": "UCS",
        "browserSecurityHeaders": {
            "contentSecurityPolicyReportOnly": "",
            "xContentTypeOptions": "nosniff",
            "xRobotsTag": "none",
            "xFrameOptions": "",  # we want to overwrite default value, which is: SAMEORIGIN
            "xXSSProtection": "1; mode=block",
            "strictTransportSecurity": "max-age=31536000; includeSubDomains",
        },
    }
    kc_admin.update_realm(DEFAULT_REALM, payload=realm_payload)

    # create ucs realm
    realm_payload["id"] = opt.realm
    realm_payload["realm"] = opt.realm
    kc_admin.create_realm(payload=realm_payload, skip_exists=True)

    # create portal saml client
    opt.metadata_url = f"https://{opt.umc_saml_sp_server}/univention/saml/metadata"
    opt.metadata_file = None
    opt.umc_uid_mapper = True
    opt.single_logout_service_url_post = None
    opt.assertion_consumer_url_post = None
    opt.frontchannel_logout_off = None
    opt.name_id_format = None
    opt.idp_initiated_sso_url_name = None
    opt.description = "Univention Management Console"
    opt.valid_redirect_uris = None
    opt.not_enabled = False
    opt.policy_url = None
    create_SAML_client(opt)

    # user federation ldap provider payload
    ldap_federation_payload = {
        "name": "ldap-provider",
        "providerId": "ldap",
        "providerType": "org.keycloak.storage.UserStorageProvider",
        "parentId": get_realm_id(kc_admin, opt.realm),
        "config": {
            "pagination": ["true"],
            "fullSyncPeriod": ["-1"],
            "startTls": ["true"],
            "connectionPooling": ["true"],
            "usersDn": [opt.ldap_base],
            "cachePolicy": ["MAX_LIFESPAN"],
            "maxLifespan": ["300000"],
            "useKerberosForPasswordAuthentication": ["false"],
            "importEnabled": ["false"],
            "enabled": ["true"],
            "bindCredential": [opt.ldap_system_user_pwd],
            "bindDn": [opt.ldap_system_user_dn],
            "changedSyncPeriod": ["-1"],
            "usernameLDAPAttribute": ["uid"],
            "vendor": ["other"],
            "uuidLDAPAttribute": ["entryUUID"],
            "allowKerberosAuthentication": ["true"],
            "kerberosRealm": [opt.kerberos_realm],
            "serverPrincipal": [f"HTTP/{opt.keycloak_fqdn}@{opt.kerberos_realm}"],
            "keyTab": [KERBEROS_KEYTAB_PATH],
            "connectionUrl": [ucs_ldap_url],
            "syncRegistrations": ["false"],
            "authType": ["simple"],
            "debug": ["false"],
            "searchScope": ["2"],
            "useTruststoreSpi": ["ldapsOnly"],
            "usePasswordModifyExtendedOp": ["true"],
            "trustEmail": ["false"],
            "priority": ["0"],
            "userObjectClasses": ["inetOrgPerson, organizationalPerson"],
            "rdnLDAPAttribute": ["uid"],
            "editMode": ["READ_ONLY"],
            "validatePasswordPolicy": ["false"],
            "batchSizeForSync": ["1000"],
        },
    }

    # find existing ldap provider or just create if none exists
    ldap_component_id = check_and_create_component(kc_admin, ldap_federation_payload["name"], ldap_federation_payload["providerId"], ldap_federation_payload)
    print(f"LDAP User Federation Added: {ucs_ldap_url}")

    # User federation mapper (LDAP)
    payload_ldap_mapper = {
        "name": "uid",
        "parentId": ldap_component_id,
        "providerId": "user-attribute-ldap-mapper",
        "providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
        "config": {
            "ldap.attribute": [
                "uid",
            ],
            "is.mandatory.in.ldap": [
                "false",
            ],
            "read.only": [
                "true",
            ],
            "user.model.attribute": [
                "uid",
            ],
        },
    }

    firstname_ldap_mapper = {
        "name": "first name",
        "parentId": ldap_component_id,
        "providerId": "user-attribute-ldap-mapper",
        "providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
        "config": {
            "ldap.attribute": [
                "givenName",
            ],
            "is.mandatory.in.ldap": [
                "false",
            ],
            "read.only": [
                "true",
            ],
            "user.model.attribute": [
                "firstName",
            ],
        },
    }
    mail_ldap_mapper = {
        "name": "email",
        "parentId": ldap_component_id,
        "providerId": "user-attribute-ldap-mapper",
        "providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
        "config": {
            "ldap.attribute": [
                "mailPrimaryAddress",
            ],
            "is.mandatory.in.ldap": [
                "false",
            ],
            "read.only": [
                "true",
            ],
            "user.model.attribute": [
                "email",
            ],
        },
    }
    lastname_ldap_mapper = {
        "name": "last name",
        "parentId": ldap_component_id,
        "providerId": "user-attribute-ldap-mapper",
        "providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
        "config": {
            "ldap.attribute": [
                "sn",
            ],
            "is.mandatory.in.ldap": [
                "false",
            ],
            "read.only": [
                "true",
            ],
            "user.model.attribute": [
                "lastName",
            ],
        },
    }
    displayname_ldap_mapper = {
        "name": "displayName",
        "parentId": ldap_component_id,
        "providerId": "user-attribute-ldap-mapper",
        "providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
        "config": {
            "ldap.attribute": [
                "displayName",
            ],
            "is.mandatory.in.ldap": [
                "false",
            ],
            "read.only": [
                "true",
            ],
            "user.model.attribute": [
                "displayName",
            ],
        },
    }
    entryuuid_ldap_mapper = {
        "name": "entryUUID",
        "parentId": ldap_component_id,
        "providerId": "user-attribute-ldap-mapper",
        "providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
        "config": {
            "ldap.attribute": [
                "entryUUID",
            ],
            "is.mandatory.in.ldap": [
                "false",
            ],
            "read.only": [
                "true",
            ],
            "user.model.attribute": [
                "entryUUID",
            ],
        },
    }

    # find existing uid->uid mapper or create if none exits
    check_and_create_component(kc_admin, payload_ldap_mapper["name"], payload_ldap_mapper["providerId"], payload_ldap_mapper)

    # Modify default mappers to correct ones
    modify_component(kc_admin, firstname_ldap_mapper)
    modify_component(kc_admin, mail_ldap_mapper)
    modify_component(kc_admin, lastname_ldap_mapper)
    check_and_create_component(kc_admin, displayname_ldap_mapper["name"], displayname_ldap_mapper["providerId"], displayname_ldap_mapper)
    check_and_create_component(kc_admin, entryuuid_ldap_mapper["name"], entryuuid_ldap_mapper["providerId"], entryuuid_ldap_mapper)

    # Setting admin level to all Domain Admins
    # Change realm to master
    kc_admin.realm_name = DEFAULT_REALM

    # Admins federation ldap provider payload
    ldap_federation_payload["name"] = "ldap-master-admin"
    ldap_federation_payload["parentId"] = get_realm_id(kc_admin, DEFAULT_REALM)
    ldap_federation_payload["config"]["customUserSearchFilter"] = [f"(memberOf=cn=Domain Admins,cn=groups,{opt.ldap_base})"]

    # find existing ldap provider or just create if none exists
    ldap_component_id = check_and_create_component(kc_admin, ldap_federation_payload["name"], ldap_federation_payload["providerId"], ldap_federation_payload)
    print(f"LDAP Domain Admins Federation Added: {ucs_ldap_url}")
    print("Filter: {}".format(ldap_federation_payload["config"]["customUserSearchFilter"]))

    payload_ldap_mapper["name"] = "admin-role"
    payload_ldap_mapper["parentId"] = ldap_component_id
    payload_ldap_mapper["providerId"] = "hardcoded-ldap-role-mapper"
    payload_ldap_mapper["config"] = {"role": ["admin"]}

    # find existing mapper or create if none exits
    check_and_create_component(kc_admin, payload_ldap_mapper["name"], payload_ldap_mapper["providerId"], payload_ldap_mapper)
    firstname_ldap_mapper["parentId"] = ldap_component_id
    mail_ldap_mapper["parentId"] = ldap_component_id
    lastname_ldap_mapper["parentId"] = ldap_component_id
    modify_component(kc_admin, firstname_ldap_mapper)
    modify_component(kc_admin, mail_ldap_mapper)
    modify_component(kc_admin, lastname_ldap_mapper)

    # register all extensions
    opt.names = DEFAULT_EXTENTIONS
    print(f"Register extenions: {' '.join(opt.names)}")
    register_extensions(opt)

    if IS_UCS:
        # save current keycloak version as domain config version
        # and domain init version
        kdc = KeycloakDomainConfig(opt.binddn, opt.bindpwd)
        print(f"Setting domain config version to {kdc.current_version}")
        kdc.set_domain_config_version(kdc.current_version)
        kdc.set_domain_config_init(kdc.current_version)


def main() -> int:
    """CLI tool to interact with Keycloak."""
    opt = parse_args()
    return opt.func(opt) or 0


if __name__ == "__main__":
    sys.exit(main())
