diff --git a/docker-jans-auth-server/requirements.txt b/docker-jans-auth-server/requirements.txt index 195561e867d..4ff0ec2e991 100644 --- a/docker-jans-auth-server/requirements.txt +++ b/docker-jans-auth-server/requirements.txt @@ -1,4 +1,4 @@ # pinned to py3-grpcio version to avoid failure on native extension build grpcio==1.41.0 libcst<0.4 -git+https://github.com/JanssenProject/jans@9b536ab2b5d398a41733790f2eeb70339f993fb7#egg=jans-pycloudlib&subdirectory=jans-pycloudlib +git+https://github.com/JanssenProject/jans@42f2834680de9578bce5f39ac5de35de19a6c48d#egg=jans-pycloudlib&subdirectory=jans-pycloudlib diff --git a/docker-jans-certmanager/requirements.txt b/docker-jans-certmanager/requirements.txt index 7ae3ec031cc..4b0061abce6 100644 --- a/docker-jans-certmanager/requirements.txt +++ b/docker-jans-certmanager/requirements.txt @@ -2,4 +2,4 @@ grpcio==1.41.0 click==6.7 libcst<0.4 -git+https://github.com/JanssenProject/jans@9b536ab2b5d398a41733790f2eeb70339f993fb7#egg=jans-pycloudlib&subdirectory=jans-pycloudlib +git+https://github.com/JanssenProject/jans@42f2834680de9578bce5f39ac5de35de19a6c48d#egg=jans-pycloudlib&subdirectory=jans-pycloudlib diff --git a/docker-jans-config-api/.dockerignore b/docker-jans-config-api/.dockerignore index e09c0544c8d..a4d23715ba3 100644 --- a/docker-jans-config-api/.dockerignore +++ b/docker-jans-config-api/.dockerignore @@ -9,3 +9,4 @@ !scripts !LICENSE !requirements.txt +!templates diff --git a/docker-jans-config-api/.hadolint.yaml b/docker-jans-config-api/.hadolint.yaml index 428f8174ee2..06778dd27f3 100644 --- a/docker-jans-config-api/.hadolint.yaml +++ b/docker-jans-config-api/.hadolint.yaml @@ -2,3 +2,4 @@ ignored: - DL3018 # Pin versions in apk add - DL3013 # Pin versions in pip - DL3003 # Use WORKDIR to switch to a directory + - SC2016 diff --git a/docker-jans-config-api/Dockerfile b/docker-jans-config-api/Dockerfile index c17eeddafd7..ad0309b7bbb 100644 --- a/docker-jans-config-api/Dockerfile +++ b/docker-jans-config-api/Dockerfile @@ -27,12 +27,23 @@ RUN wget -q https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-home/${JETTY_ && mv /opt/jetty-home-${JETTY_VERSION} ${JETTY_HOME} \ && rm -rf /tmp/jetty.tar.gz +# ====== +# Jython +# ====== + +ARG JYTHON_VERSION=2.7.3 +ARG JYTHON_BUILD_DATE='2022-08-01 07:49' +RUN wget -q https://maven.jans.io/maven/io/jans/jython-installer/${JYTHON_VERSION}/jython-installer-${JYTHON_VERSION}.jar -O /tmp/jython-installer.jar \ + && mkdir -p /opt/jython \ + && java -jar /tmp/jython-installer.jar -v -s -d /opt/jython \ + && rm -f /tmp/jython-installer.jar /tmp/*.properties + # ========== # Config API # ========== ENV CN_VERSION=1.0.3-SNAPSHOT -ENV CN_BUILD_DATE='2022-10-14 16:35' +ENV CN_BUILD_DATE='2022-10-24 13:36' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-config-api-server/${CN_VERSION}/jans-config-api-server-${CN_VERSION}.war # Install Jans Config API @@ -99,6 +110,41 @@ RUN mkdir -p /opt/prometheus \ && wget -q https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/${PROMETHEUS_JAVAAGENT_VERSION}/jmx_prometheus_javaagent-${PROMETHEUS_JAVAAGENT_VERSION}.jar -O /opt/prometheus/jmx_prometheus_javaagent.jar \ && java -jar ${JETTY_HOME}/start.jar jetty.home=${JETTY_HOME} jetty.base=${JETTY_BASE}/jans-config-api --add-module=jmx,stats +# ===================== +# jans-linux-setup sync +# ===================== + +ENV JANS_SOURCE_VERSION=e74ea8e27e59d35ff6e3c6f997e6c1df6a04ec83 +ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup +ARG JANS_CONFIG_API_DOCS=jans-config-api/docs + +# note that as we're pulling from a monorepo (with multiple project in it) +# we are using partial-clone and sparse-checkout to get the jans-linux-setup code +RUN git clone --filter blob:none --no-checkout https://github.com/janssenproject/jans /tmp/jans \ + && cd /tmp/jans \ + && git sparse-checkout init --cone \ + && git checkout ${JANS_SOURCE_VERSION} \ + && git sparse-checkout add ${JANS_SETUP_DIR} \ + && git sparse-checkout add ${JANS_CONFIG_API_DOCS} + +RUN mkdir -p /etc/jans/conf \ + /app/static/rdbm \ + /app/schema \ + /app/templates/jans-config-api + +# sync static files from linux-setup +RUN cd /tmp/jans \ + && cp ${JANS_SETUP_DIR}/static/rdbm/sql_data_types.json /app/static/rdbm/ \ + && cp ${JANS_SETUP_DIR}/static/rdbm/ldap_sql_data_type_mapping.json /app/static/rdbm/ \ + && cp ${JANS_SETUP_DIR}/static/rdbm/opendj_attributes_syntax.json /app/static/rdbm/ \ + && cp ${JANS_SETUP_DIR}/static/rdbm/sub_tables.json /app/static/rdbm/ \ + && cp ${JANS_SETUP_DIR}/schema/jans_schema.json /app/schema/ \ + && cp ${JANS_SETUP_DIR}/schema/custom_schema.json /app/schema/ \ + && cp ${JANS_SETUP_DIR}/schema/opendj_types.json /app/schema/ \ + && cp ${JANS_SETUP_DIR}/templates/jans-config-api/config.ldif /app/templates/jans-config-api/ \ + && cp ${JANS_SETUP_DIR}/templates/jans-config-api/dynamic-conf.json /app/templates/jans-config-api/ \ + && cp ${JANS_CONFIG_API_DOCS}/jans-config-api-swagger-auto.yaml /app/static/ + # ======= # Cleanup # ======= @@ -212,9 +258,13 @@ RUN touch /etc/hosts.back COPY jetty/log4j2.xml ${JETTY_BASE}/jans-config-api/resources/ COPY conf/*.tmpl /app/templates/ COPY plugins /app/plugins +COPY templates /app/templates COPY scripts /app/scripts RUN chmod +x /app/scripts/entrypoint.sh +# unquote apiApprovedIssuer +RUN sed -i 's/"${apiApprovedIssuer}"/${apiApprovedIssuer}/g' /app/templates/jans-config-api/dynamic-conf.json + # create non-root user RUN adduser -s /bin/sh -D -G root -u 1000 jetty @@ -229,7 +279,8 @@ RUN chmod -R g=u ${JETTY_BASE}/jans-config-api/custom \ && chmod 664 /etc/hosts.back \ && chmod 664 /usr/java/latest/jre/lib/security/cacerts \ && chmod 664 /opt/jetty/etc/jetty.xml \ - && chmod 664 /opt/jetty/etc/webdefault.xml + && chmod 664 /opt/jetty/etc/webdefault.xml \ + && chmod -R g=u /app/templates/jans-config-api USER 1000 diff --git a/docker-jans-config-api/README.md b/docker-jans-config-api/README.md index 121faaca18c..e320373464e 100644 --- a/docker-jans-config-api/README.md +++ b/docker-jans-config-api/README.md @@ -70,6 +70,7 @@ The following environment variables are supported by the container: - `CN_CONFIG_API_APP_LOGGERS`: Custom logging configuration in JSON-string format with hash type (see [Configure app loggers](#configure-app-loggers) section for details). - `CN_CONFIG_API_PLUGINS`: Comma-separated plugin names that should be enabled (available plugins are `admin-ui`, `scim`, `fido2`, and `user-mgt`). Note that unknown plugin name will be ignored. - `CN_TOKEN_SERVER_CERT_FILE`: Path to token server certificate (default to `/etc/certs/token_server.crt`). +- `CN_TOKEN_SERVER_BASE_HOSTNAME`: Hostname of token server (default to empty string). - `CN_ADMIN_UI_PLUGIN_LOGGERS`: Custom logging configuration for AdminUI plugin in JSON-string format with hash type (see [Configure plugin loggers](#configure-plugin-loggers) section for details). - `CN_PROMETHEUS_PORT`: Port used by Prometheus JMX agent (default to empty string). To enable Prometheus JMX agent, set the value to a number. See [Exposing metrics](#exposing-metrics) for details. - `CN_SQL_DB_HOST`: Hostname of the SQL database (default to `localhost`). diff --git a/docker-jans-config-api/requirements.txt b/docker-jans-config-api/requirements.txt index 195561e867d..ba680ac387d 100644 --- a/docker-jans-config-api/requirements.txt +++ b/docker-jans-config-api/requirements.txt @@ -1,4 +1,5 @@ # pinned to py3-grpcio version to avoid failure on native extension build grpcio==1.41.0 libcst<0.4 -git+https://github.com/JanssenProject/jans@9b536ab2b5d398a41733790f2eeb70339f993fb7#egg=jans-pycloudlib&subdirectory=jans-pycloudlib +ruamel.yaml==0.16.10 +git+https://github.com/JanssenProject/jans@42f2834680de9578bce5f39ac5de35de19a6c48d#egg=jans-pycloudlib&subdirectory=jans-pycloudlib diff --git a/docker-jans-config-api/scripts/bootstrap.py b/docker-jans-config-api/scripts/bootstrap.py index 3b5c66d6df6..ac923a0e997 100644 --- a/docker-jans-config-api/scripts/bootstrap.py +++ b/docker-jans-config-api/scripts/bootstrap.py @@ -2,7 +2,13 @@ import logging.config import os import re +import typing as _t +from functools import cached_property from string import Template +from urllib.parse import urlparse +from uuid import uuid4 + +from ldif import LDIFWriter from jans.pycloudlib import get_manager from jans.pycloudlib.persistence import render_couchbase_properties @@ -14,12 +20,23 @@ from jans.pycloudlib.persistence import sync_ldap_truststore from jans.pycloudlib.persistence import render_sql_properties from jans.pycloudlib.persistence import render_spanner_properties +from jans.pycloudlib.persistence.couchbase import CouchbaseClient +from jans.pycloudlib.persistence.couchbase import id_from_dn +from jans.pycloudlib.persistence.ldap import LdapClient +from jans.pycloudlib.persistence.spanner import SpannerClient +from jans.pycloudlib.persistence.sql import SqlClient +from jans.pycloudlib.persistence.sql import doc_id_from_dn from jans.pycloudlib.persistence.utils import PersistenceMapper from jans.pycloudlib.utils import cert_to_truststore +from jans.pycloudlib.utils import generate_base64_contents +from jans.pycloudlib.utils import get_random_chars +from jans.pycloudlib.utils import encode_text from settings import LOGGING_CONFIG from plugins import AdminUiPlugin from plugins import discover_plugins +from utils import parse_config_api_swagger +from utils import generate_hex logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("entrypoint") @@ -89,6 +106,9 @@ def main(): modify_webdefault_xml() configure_logging() + persistence_setup = PersistenceSetup(manager) + persistence_setup.import_ldif_files() + plugins = discover_plugins() logger.info(f"Loaded config-api plugins: {', '.join(plugins)}") modify_config_api_xml(plugins) @@ -287,5 +307,188 @@ def configure_admin_ui_logging(): f.write(tmpl.safe_substitute(config)) +class PersistenceSetup: + def __init__(self, manager) -> None: + self.manager = manager + + client_classes = { + "ldap": LdapClient, + "couchbase": CouchbaseClient, + "spanner": SpannerClient, + "sql": SqlClient, + } + + # determine persistence type + mapper = PersistenceMapper() + self.persistence_type = mapper.mapping["default"] + + # determine persistence client + client_cls = client_classes.get(self.persistence_type) + self.client = client_cls(manager) + + def get_auth_config(self): + dn = "ou=jans-auth,ou=configuration,o=jans" + + # sql and spanner + if self.persistence_type in ("sql", "spanner"): + entry = self.client.get("jansAppConf", doc_id_from_dn(dn)) + return json.loads(entry["jansConfDyn"]) + + # couchbase + elif self.persistence_type == "couchbase": + key = id_from_dn(dn) + bucket = os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans") + req = self.client.exec_query( + f"SELECT META().id, {bucket}.* FROM {bucket} USE KEYS '{key}'" + ) + attrs = req.json()["results"][0] + return attrs["jansConfDyn"] + + # ldap + else: + entry = self.client.get(dn, attributes=["jansConfDyn"]) + return json.loads(entry.entry_attributes_as_dict["jansConfDyn"][0]) + + def transform_url(self, url): + auth_server_url = os.environ.get("CN_AUTH_SERVER_URL", "") + + if not auth_server_url: + return url + + parse_result = urlparse(url) + if parse_result.path.startswith("/.well-known"): + path = f"/jans-auth{parse_result.path}" + else: + path = parse_result.path + url = f"http://{auth_server_url}{path}" + return url + + def get_injected_urls(self): + auth_config = self.get_auth_config() + + urls = ( + "issuer", + "openIdConfigurationEndpoint", + "introspectionEndpoint", + "tokenEndpoint", + "tokenRevocationEndpoint", + ) + + return { + url: self.transform_url(auth_config[url]) + for url in urls + } + + @cached_property + def ctx(self) -> dict[str, _t.Any]: + hostname = self.manager.config.get("hostname") + approved_issuer = [hostname] + + token_server_hostname = os.environ.get("CN_TOKEN_SERVER_BASE_HOSTNAME") + if token_server_hostname and token_server_hostname not in approved_issuer: + approved_issuer.append(token_server_hostname) + + ctx = { + "hostname": hostname, + "apiApprovedIssuer": ",".join([f'"https://{issuer}"' for issuer in approved_issuer]), + "apiProtectionType": "oauth2", + "endpointInjectionEnabled": "true", + "configOauthEnabled": str(os.environ.get("CN_CONFIG_API_OAUTH_ENABLED") or True).lower(), + } + ctx.update(self.get_injected_urls()) + + # Client + ctx["jca_client_id"] = self.manager.config.get("jca_client_id") + if not ctx["jca_client_id"]: + ctx["jca_client_id"] = f"1800.{uuid4()}" + self.manager.config.set("jca_client_id", ctx["jca_client_id"]) + + ctx["jca_client_pw"] = self.manager.secret.get("jca_client_pw") + if not ctx["jca_client_pw"]: + ctx["jca_client_pw"] = get_random_chars() + self.manager.secret.set("jca_client_pw", ctx["jca_client_pw"]) + + ctx["jca_client_encoded_pw"] = self.manager.secret.get("jca_client_encoded_pw") + if not ctx["jca_client_encoded_pw"]: + ctx["jca_client_encoded_pw"] = encode_text( + ctx["jca_client_pw"], self.manager.secret.get("encoded_salt"), + ).decode() + self.manager.secret.set("jca_client_encoded_pw", ctx["jca_client_encoded_pw"]) + + # pre-populate config_api_dynamic_conf_base64 + with open("/app/templates/jans-config-api/dynamic-conf.json") as f: + tmpl = Template(f.read()) + ctx["config_api_dynamic_conf_base64"] = generate_base64_contents(tmpl.substitute(**ctx)) + + # finalize ctx + return ctx + + def get_scope_jans_ids(self): + if self.persistence_type in ("sql", "spanner"): + entries = self.client.search("jansScope", ["jansId"]) + return [entry["jansId"] for entry in entries] + + if self.persistence_type == "couchbase": + bucket = os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans") + req = self.client.exec_query( + f"SELECT {bucket}.jansId FROM {bucket} WHERE objectClass = 'jansScope'", + ) + results = req.json()["results"] + return [item["jansId"] for item in results] + + # likely ldap + entries = self.client.search("ou=scopes,o=jans", "(objectClass=jansScope)", ["jansId"]) + return [entry.entry_attributes_as_dict["jansId"][0] for entry in entries] + + def generate_scopes_ldif(self): + # jansId to compare to + existing_jans_ids = self.get_scope_jans_ids() + + def generate_config_api_scopes(): + swagger = parse_config_api_swagger() + scopes = swagger["components"]["securitySchemes"]["oauth2"]["flows"]["clientCredentials"]["scopes"] + + generated_scopes = [] + for jans_id, desc in scopes.items(): + if jans_id in existing_jans_ids: + continue + + inum = f"1800.{generate_hex()}-{generate_hex()}" + attrs = { + "creatorAttrs": [json.dumps({})], + "description": [desc], + "displayName": [f"Config API scope {jans_id}"], + "inum": [inum], + "jansAttrs": [json.dumps({"spontaneousClientScopes": None, "showInConfigurationEndpoint": True})], + "jansId": [jans_id], + "jansScopeTyp": ["oauth"], + "objectClass": ["top", "jansScope"], + "jansDefScope": ["false"], + } + generated_scopes.append(attrs) + return generated_scopes + + # prepare required scopes (if any) + scopes = [] + + config_api_scopes = generate_config_api_scopes() + scopes += config_api_scopes + + with open("/app/templates/jans-config-api/scopes.ldif", "wb") as fd: + writer = LDIFWriter(fd, cols=1000) + for scope in scopes: + writer.unparse(f"inum={scope['inum'][0]},ou=scopes,o=jans", scope) + + def import_ldif_files(self) -> None: + self.generate_scopes_ldif() + + files = ["config.ldif", "scopes.ldif", "clients.ldif"] + ldif_files = [f"/app/templates/jans-config-api/{file_}" for file_ in files] + + for file_ in ldif_files: + logger.info(f"Importing {file_}") + self.client.create_from_ldif(file_, self.ctx) + + if __name__ == "__main__": main() diff --git a/docker-jans-config-api/scripts/entrypoint.sh b/docker-jans-config-api/scripts/entrypoint.sh index 880c7fc0ff0..1344046e54a 100644 --- a/docker-jans-config-api/scripts/entrypoint.sh +++ b/docker-jans-config-api/scripts/entrypoint.sh @@ -24,6 +24,7 @@ get_prometheus_opt() { python3 /app/scripts/wait.py python3 /app/scripts/bootstrap.py +python3 /app/scripts/upgrade.py # run config-api cd /opt/jans/jetty/jans-config-api @@ -35,6 +36,7 @@ exec java \ -Djans.base=/etc/jans \ -Dserver.base=/opt/jans/jetty/jans-config-api \ -Dlog.base=/opt/jans/jetty/jans-config-api \ + -Dpython.home=/opt/jython \ -Djava.io.tmpdir=/tmp \ -Dlog4j2.configurationFile=$(get_logging_files) \ $(get_prometheus_opt) \ diff --git a/docker-jans-config-api/scripts/upgrade.py b/docker-jans-config-api/scripts/upgrade.py new file mode 100644 index 00000000000..f2f851b0701 --- /dev/null +++ b/docker-jans-config-api/scripts/upgrade.py @@ -0,0 +1,432 @@ +import contextlib +import json +import logging.config +import os +from collections import namedtuple + +from ldif import LDIFWriter + +from jans.pycloudlib import get_manager +from jans.pycloudlib.persistence import CouchbaseClient +from jans.pycloudlib.persistence import LdapClient +from jans.pycloudlib.persistence import SpannerClient +from jans.pycloudlib.persistence import SqlClient +from jans.pycloudlib.persistence import PersistenceMapper +from jans.pycloudlib.persistence import doc_id_from_dn +from jans.pycloudlib.persistence import id_from_dn + +from settings import LOGGING_CONFIG +from utils import parse_config_api_swagger +from utils import generate_hex + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("entrypoint") + +Entry = namedtuple("Entry", ["id", "attrs"]) + + +def _transform_api_dynamic_config(conf): + should_update = False + + if "userExclusionAttributes" not in conf: + conf["userExclusionAttributes"] = ["userPassword"] + should_update = True + + if "userMandatoryAttributes" not in conf: + conf["userMandatoryAttributes"] = [ + "mail", + "displayName", + "jansStatus", + "userPassword", + "givenName", + ] + should_update = True + + if "agamaConfiguration" not in conf: + conf["agamaConfiguration"] = { + "mandatoryAttributes": [ + "qname", + "source", + ], + "optionalAttributes": [ + "serialVersionUID", + "enabled", + ], + } + should_update = True + return conf, should_update + + +class LDAPBackend: + def __init__(self, manager): + self.manager = manager + self.client = LdapClient(manager) + self.type = "ldap" + + def format_attrs(self, attrs): + _attrs = {} + for k, v in attrs.items(): + if len(v) < 2: + v = v[0] + _attrs[k] = v + return _attrs + + def get_entry(self, key, filter_="", attrs=None, **kwargs): + filter_ = filter_ or "(objectClass=*)" + + entry = self.client.get(key, filter_=filter_, attributes=attrs) + if not entry: + return None + return Entry(entry.entry_dn, self.format_attrs(entry.entry_attributes_as_dict)) + + def modify_entry(self, key, attrs=None, **kwargs): + attrs = attrs or {} + del_flag = kwargs.get("delete_attr", False) + + if del_flag: + mod = self.client.MODIFY_DELETE + else: + mod = self.client.MODIFY_REPLACE + + for k, v in attrs.items(): + if not isinstance(v, list): + v = [v] + attrs[k] = [(mod, v)] + return self.client.modify(key, attrs) + + def search_entries(self, key, filter_="", attrs=None, **kwargs): + filter_ = filter_ or "(objectClass=*)" + entries = self.client.search(key, filter_, attrs) + + return [ + Entry(entry.entry_dn, self.format_attrs(entry.entry_attributes_as_dict)) + for entry in entries + ] + + +class SQLBackend: + def __init__(self, manager): + self.manager = manager + self.client = SqlClient(manager) + self.type = "sql" + + def get_entry(self, key, filter_="", attrs=None, **kwargs): + table_name = kwargs.get("table_name") + entry = self.client.get(table_name, key, attrs) + + if not entry: + return None + return Entry(key, entry) + + def modify_entry(self, key, attrs=None, **kwargs): + attrs = attrs or {} + table_name = kwargs.get("table_name") + return self.client.update(table_name, key, attrs), "" + + def search_entries(self, key, filter_="", attrs=None, **kwargs): + attrs = attrs or {} + table_name = kwargs.get("table_name") + return [ + Entry(entry["doc_id"], entry) + for entry in self.client.search(table_name, attrs) + ] + + +class CouchbaseBackend: + def __init__(self, manager): + self.manager = manager + self.client = CouchbaseClient(manager) + self.type = "couchbase" + + def get_entry(self, key, filter_="", attrs=None, **kwargs): + bucket = kwargs.get("bucket") + req = self.client.exec_query( + f"SELECT META().id, {bucket}.* FROM {bucket} USE KEYS '{key}'" + ) + if not req.ok: + return + + try: + _attrs = req.json()["results"][0] + id_ = _attrs.pop("id") + entry = Entry(id_, _attrs) + except IndexError: + entry = None + return entry + + def modify_entry(self, key, attrs=None, **kwargs): + bucket = kwargs.get("bucket") + del_flag = kwargs.get("delete_attr", False) + attrs = attrs or {} + + if del_flag: + kv = ",".join(attrs.keys()) + mod_kv = f"UNSET {kv}" + else: + kv = ",".join([ + "{}={}".format(k, json.dumps(v)) + for k, v in attrs.items() + ]) + mod_kv = f"SET {kv}" + + query = f"UPDATE {bucket} USE KEYS '{key}' {mod_kv}" + req = self.client.exec_query(query) + + if req.ok: + resp = req.json() + status = bool(resp["status"] == "success") + message = resp["status"] + else: + status = False + message = req.text or req.reason + return status, message + + def search_entries(self, key, filter_="", attrs=None, **kwargs): + bucket = kwargs.get("bucket") + req = self.client.exec_query( + f"SELECT META().id, {bucket}.* FROM {bucket} {filter_}" + ) + if not req.ok: + return [] + + entries = [] + for item in req.json()["results"]: + id_ = item.pop("id") + entries.append(Entry(id_, item)) + return entries + + +class SpannerBackend: + def __init__(self, manager): + self.manager = manager + self.client = SpannerClient(manager) + self.type = "spanner" + + def get_entry(self, key, filter_="", attrs=None, **kwargs): + table_name = kwargs.get("table_name") + entry = self.client.get(table_name, key, attrs) + + if not entry: + return None + return Entry(key, entry) + + def modify_entry(self, key, attrs=None, **kwargs): + attrs = attrs or {} + table_name = kwargs.get("table_name") + return self.client.update(table_name, key, attrs), "" + + def search_entries(self, key, filter_="", attrs=None, **kwargs): + attrs = attrs or {} + table_name = kwargs.get("table_name") + return [ + Entry(entry["doc_id"], entry) + for entry in self.client.search(table_name, attrs) + ] + + +BACKEND_CLASSES = { + "sql": SQLBackend, + "couchbase": CouchbaseBackend, + "spanner": SpannerBackend, + "ldap": LDAPBackend, +} + + +class Upgrade: + def __init__(self, manager): + self.manager = manager + + mapper = PersistenceMapper() + + backend_cls = BACKEND_CLASSES[mapper.mapping["default"]] + self.backend = backend_cls(manager) + + def invoke(self): + logger.info("Running upgrade process (if required)") + self.update_client_redirect_uri() + self.update_api_dynamic_config() + self.update_client_scopes() + + def update_client_redirect_uri(self): + kwargs = {} + jca_client_id = self.manager.config.get("jca_client_id") + id_ = f"inum={jca_client_id},ou=clients,o=jans" + + if self.backend.type in ("sql", "spanner"): + kwargs = {"table_name": "jansClnt"} + id_ = doc_id_from_dn(id_) + elif self.backend.type == "couchbase": + kwargs = {"bucket": os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans")} + id_ = id_from_dn(id_) + + entry = self.backend.get_entry(id_, **kwargs) + + if not entry: + return + + should_update = False + hostname = self.manager.config.get("hostname") + + if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if f"https://{hostname}/admin" not in entry.attrs["jansRedirectURI"]["v"]: + entry.attrs["jansRedirectURI"]["v"].append(f"https://{hostname}/admin") + should_update = True + else: # ldap, couchbase, and spanner + if f"https://{hostname}/admin" not in entry.attrs["jansRedirectURI"]: + entry.attrs["jansRedirectURI"].append(f"https://{hostname}/admin") + should_update = True + + if should_update: + self.backend.modify_entry(entry.id, entry.attrs, **kwargs) + + def update_api_dynamic_config(self): + kwargs = {} + id_ = "ou=jans-config-api,ou=configuration,o=jans" + + if self.backend.type in ("sql", "spanner"): + kwargs = {"table_name": "jansAppConf"} + id_ = doc_id_from_dn(id_) + elif self.backend.type == "couchbase": + kwargs = {"bucket": os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans")} + id_ = id_from_dn(id_) + + entry = self.backend.get_entry(id_, **kwargs) + + if not entry: + return + + if self.backend.type != "couchbase": + with contextlib.suppress(json.decoder.JSONDecodeError): + entry.attrs["jansConfDyn"] = json.loads(entry.attrs["jansConfDyn"]) + + conf, should_update = _transform_api_dynamic_config(entry.attrs["jansConfDyn"]) + + if should_update: + if self.backend.type != "couchbase": + entry.attrs["jansConfDyn"] = json.dumps(conf) + + entry.attrs["jansRevision"] += 1 + self.backend.modify_entry(entry.id, entry.attrs, **kwargs) + + def get_all_scopes(self): + if self.backend.type in ("sql", "spanner"): + kwargs = {"table_name": "jansScope"} + entries = self.backend.search_entries(None, **kwargs) + elif self.backend.type == "couchbase": + kwargs = {"bucket": os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans")} + entries = self.backend.search_entries( + None, filter_="WHERE objectClass = 'jansScope'", **kwargs + ) + else: + # likely ldap + entries = self.backend.search_entries( + "ou=scopes,o=jans", filter_="(objectClass=jansScope)" + ) + + return { + entry.attrs["jansId"]: entry.attrs.get("dn") or entry.id + for entry in entries + } + + def generate_scim_plugin_scopes(self): + all_scopes = self.get_all_scopes() + plugin_scopes = { + "https://jans.io/scim/users.read": "Query user resources", + "https://jans.io/scim/users.write": "Manage user resources", + } + generated_scopes = [] + + for jans_id, desc in plugin_scopes.items(): + if jans_id in all_scopes: + continue + + inum = f"1200.{generate_hex()}-{generate_hex()}" + attrs = { + "description": [desc], + "displayName": [f"SCIM scope {jans_id}"], + "inum": [inum], + "jansAttrs": [json.dumps({"spontaneousClientScopes": None, "showInConfigurationEndpoint": True})], + "jansId": [jans_id], + "jansScopeTyp": ["oauth"], + "objectClass": ["top", "jansScope"], + "jansDefScope": ["false"], + } + generated_scopes.append(attrs) + + with open("/app/templates/jans-config-api/scim-scopes.ldif", "wb") as fd: + writer = LDIFWriter(fd) + + for scope in generated_scopes: + writer.unparse(f"inum={scope['inum'][0]},ou=scopes,o=jans", scope) + self.backend.client.create_from_ldif("/app/templates/jans-config-api/scim-scopes.ldif", {}) + + def update_client_scopes(self): + kwargs = {} + client_id = self.manager.config.get("jca_client_id") + id_ = f"inum={client_id},ou=clients,o=jans" + + if self.backend.type in ("sql", "spanner"): + kwargs = {"table_name": "jansClnt"} + id_ = doc_id_from_dn(id_) + elif self.backend.type == "couchbase": + kwargs = {"bucket": os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans")} + id_ = id_from_dn(id_) + + entry = self.backend.get_entry(id_, **kwargs) + + if not entry: + return + + if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + client_scopes = entry.attrs["jansScope"]["v"] + else: + client_scopes = entry.attrs["jansScope"] + + if not isinstance(client_scopes, list): + client_scopes = [client_scopes] + + # prepare scim plugin scopes + self.generate_scim_plugin_scopes() + + # all scopes mapping from persistence + all_scopes = self.get_all_scopes() + + # all potential scopes for client + new_client_scopes = [] + + # extract config_api scopes within range of jansId defined in swagger + swagger = parse_config_api_swagger() + config_api_jans_ids = list(swagger["components"]["securitySchemes"]["oauth2"]["flows"]["clientCredentials"]["scopes"].keys()) + config_api_scopes = list({ + dn for jid, dn in all_scopes.items() + if jid in config_api_jans_ids + }) + new_client_scopes += config_api_scopes + + # extract scim scopes within range of jansId defined in swagger + scim_jans_ids = ["https://jans.io/scim/users.read", "https://jans.io/scim/users.write"] + scim_scopes = list({ + dn for jid, dn in all_scopes.items() + if jid in scim_jans_ids + }) + new_client_scopes += scim_scopes + + # find missing scopes from the client + diff = list(set(new_client_scopes).difference(client_scopes)) + + if diff: + if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + entry.attrs["jansScope"]["v"] = client_scopes + diff + else: + entry.attrs["jansScope"] = client_scopes + diff + self.backend.modify_entry(entry.id, entry.attrs, **kwargs) + + +def main(): + manager = get_manager() + upgrade = Upgrade(manager) + upgrade.invoke() + + +if __name__ == "__main__": + main() diff --git a/docker-jans-config-api/scripts/utils.py b/docker-jans-config-api/scripts/utils.py index 23eeb16cc30..e96318856ef 100644 --- a/docker-jans-config-api/scripts/utils.py +++ b/docker-jans-config-api/scripts/utils.py @@ -1,111 +1,14 @@ -import json import os -from urllib.parse import urlparse -from jans.pycloudlib import get_manager -from jans.pycloudlib.persistence.couchbase import CouchbaseClient -from jans.pycloudlib.persistence.sql import SqlClient -from jans.pycloudlib.persistence.ldap import LdapClient -from jans.pycloudlib.persistence.spanner import SpannerClient -from jans.pycloudlib.persistence.utils import PersistenceMapper +import ruamel.yaml -class LdapPersistence: - def __init__(self, manager): - self.client = LdapClient(manager) +def parse_config_api_swagger(path="/app/static/jans-config-api-swagger-auto.yaml"): + with open(path) as f: + txt = f.read() + txt = txt.replace("\t", " ") + return ruamel.yaml.load(txt, Loader=ruamel.yaml.RoundTripLoader) - def get_auth_config(self): - # base DN for auth config - dn = "ou=jans-auth,ou=configuration,o=jans" - entry = self.client.get(dn) - if not entry: - return {} - return entry["jansConfDyn"][0] - - -class CouchbasePersistence: - def __init__(self, manager): - self.client = CouchbaseClient(manager) - - def get_auth_config(self): - bucket = os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans") - req = self.client.exec_query( - f"SELECT jansConfDyn FROM `{bucket}` USE KEYS 'configuration_jans-auth'" # nosec: B608 - ) - if not req.ok: - return {} - - config = req.json()["results"][0] - if not config: - return {} - return config["jansConfDyn"] - - -class SqlPersistence: - def __init__(self, manager): - self.client = SqlClient(manager) - - def get_auth_config(self): - config = self.client.get( - "jansAppConf", - "jans-auth", - ["jansConfDyn"], - ) - return config.get("jansConfDyn", "") - - -class SpannerPersistence(SqlPersistence): - def __init__(self, manager): - self.client = SpannerClient(manager) - - -def transform_url(url): - auth_server_url = os.environ.get("CN_AUTH_SERVER_URL", "") - - if not auth_server_url: - return url - - parse_result = urlparse(url) - if parse_result.path.startswith("/.well-known"): - path = f"/jans-auth{parse_result.path}" - else: - path = parse_result.path - url = f"http://{auth_server_url}{path}" - return url - - -_backend_classes = { - "ldap": LdapPersistence, - "couchbase": CouchbasePersistence, - "sql": SqlPersistence, - "spanner": SpannerPersistence, -} - - -def get_injected_urls(): - manager = get_manager() - - # resolve backend - mapping = PersistenceMapper().mapping - backend_type = mapping["default"] - backend = _backend_classes[backend_type](manager) - - auth_config = backend.get_auth_config() - try: - auth_config = json.loads(auth_config) - except TypeError: - pass - - endpoints = [ - "issuer", - "openIdConfigurationEndpoint", - "introspectionEndpoint", - "tokenEndpoint", - "tokenRevocationEndpoint", - ] - transformed_urls = { - attr: transform_url(auth_config[attr]) - for attr in endpoints - } - return transformed_urls +def generate_hex(size: int = 3): + return os.urandom(size).hex().upper() diff --git a/docker-jans-config-api/templates/jans-config-api/clients.ldif b/docker-jans-config-api/templates/jans-config-api/clients.ldif new file mode 100644 index 00000000000..3f2297dec4c --- /dev/null +++ b/docker-jans-config-api/templates/jans-config-api/clients.ldif @@ -0,0 +1,27 @@ +dn: inum=%(jca_client_id)s,ou=clients,o=jans +del: false +displayName: Jans Config Api Client +inum: %(jca_client_id)s +jansAccessTknAsJwt: false +jansAccessTknSigAlg: RS256 +jansAppTyp: web +jansAttrs: {"tlsClientAuthSubjectDn": "", "runIntrospectionScriptBeforeJwtCreation": false, "keepClientAuthorizationAfterExpiration": false, "allowSpontaneousScopes": false, "spontaneousScopes": [], "spontaneousScopeScriptDns": [], "backchannelLogoutUri": [], "backchannelLogoutSessionRequired": false, "additionalAudience": [], "postAuthnScripts": [], "consentGatheringScripts": [], "introspectionScripts": [], "rptClaimsScripts": []} +jansClntSecret: %(jca_client_encoded_pw)s +jansDisabled: false +jansGrantTyp: authorization_code +jansGrantTyp: refresh_token +jansGrantTyp: client_credentials +jansIdTknSignedRespAlg: RS256 +jansInclClaimsInIdTkn: false +jansLogoutSessRequired: false +jansPersistClntAuthzs: true +jansRespTyp: code +jansRptAsJwt: false +jansScope: inum=C4F7,ou=scopes,o=jans +jansSubjectTyp: pairwise +jansTknEndpointAuthMethod: client_secret_basic +jansTrustedClnt: false +objectClass: top +objectClass: jansClnt +jansRedirectURI: https://%(hostname)s/admin +jansRedirectURI: http://localhost:4100 diff --git a/docker-jans-configurator/Dockerfile b/docker-jans-configurator/Dockerfile index 2ec24f880c6..ce87e3aa450 100644 --- a/docker-jans-configurator/Dockerfile +++ b/docker-jans-configurator/Dockerfile @@ -17,7 +17,7 @@ RUN apk update \ # JAR files required to generate OpenID Connect keys ENV CN_VERSION=1.0.3-SNAPSHOT -ENV CN_BUILD_DATE='2022-08-30 17:15' +ENV CN_BUILD_DATE='2022-10-24 10:01' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-auth-client/${CN_VERSION}/jans-auth-client-${CN_VERSION}-jar-with-dependencies.jar RUN wget -q ${CN_SOURCE_URL} -P /app/javalibs/ diff --git a/docker-jans-configurator/requirements.txt b/docker-jans-configurator/requirements.txt index afd0470d661..b526d8c89a6 100644 --- a/docker-jans-configurator/requirements.txt +++ b/docker-jans-configurator/requirements.txt @@ -4,4 +4,4 @@ click==6.7 marshmallow==3.10.0 fqdn==1.4.0 libcst<0.4 -git+https://github.com/JanssenProject/jans@9b536ab2b5d398a41733790f2eeb70339f993fb7#egg=jans-pycloudlib&subdirectory=jans-pycloudlib +git+https://github.com/JanssenProject/jans@42f2834680de9578bce5f39ac5de35de19a6c48d#egg=jans-pycloudlib&subdirectory=jans-pycloudlib diff --git a/docker-jans-configurator/scripts/bootstrap.py b/docker-jans-configurator/scripts/bootstrap.py index 9c7dc2df7fc..60d4bcfff86 100644 --- a/docker-jans-configurator/scripts/bootstrap.py +++ b/docker-jans-configurator/scripts/bootstrap.py @@ -351,14 +351,6 @@ def auth_ctx(self): partial(encode_text, fr.read(), encoded_salt), ) - def config_api_ctx(self): - self.set_config("jca_client_id", lambda: f"1801.{uuid4()}") - jca_client_pw = self.set_secret("jca_client_pw", get_random_chars) - self.set_secret( - "jca_client_encoded_pw", - partial(encode_text, jca_client_pw, self.get_secret("encoded_salt")) - ) - def passport_rs_ctx(self): encoded_salt = self.get_secret("encoded_salt") self.set_config("passport_rs_client_id", lambda: f"1501.{uuid4()}") @@ -755,7 +747,6 @@ def generate(self): self.base_ctx() self.auth_ctx() - self.config_api_ctx() self.web_ctx() if "ldap" in opt_scopes: @@ -778,27 +769,11 @@ def generate(self): if "sql" in opt_scopes: self.sql_ctx() - self.admin_ui_ctx() self.jans_cli_ctx() # populated config return self.ctx - def admin_ui_ctx(self): - self.set_config("admin_ui_client_id", lambda: f"1901.{uuid4()}") - admin_ui_client_pw = self.set_secret("admin_ui_client_pw", get_random_chars) - self.set_secret( - "admin_ui_client_encoded_pw", - partial(encode_text, admin_ui_client_pw, self.get_secret("encoded_salt")), - ) - - self.set_config("token_server_admin_ui_client_id", lambda: f"1901.{uuid4()}") - token_server_admin_ui_client_pw = self.set_secret("token_server_admin_ui_client_pw", get_random_chars) - self.set_secret( - "token_server_admin_ui_client_encoded_pw", - partial(encode_text, token_server_admin_ui_client_pw, self.get_secret("encoded_salt")), - ) - def jans_cli_ctx(self): self.set_config("role_based_client_id", lambda: f"2000.{uuid4()}") role_based_client_pw = self.set_secret("role_based_client_pw", get_random_chars) diff --git a/docker-jans-fido2/requirements.txt b/docker-jans-fido2/requirements.txt index 195561e867d..4ff0ec2e991 100644 --- a/docker-jans-fido2/requirements.txt +++ b/docker-jans-fido2/requirements.txt @@ -1,4 +1,4 @@ # pinned to py3-grpcio version to avoid failure on native extension build grpcio==1.41.0 libcst<0.4 -git+https://github.com/JanssenProject/jans@9b536ab2b5d398a41733790f2eeb70339f993fb7#egg=jans-pycloudlib&subdirectory=jans-pycloudlib +git+https://github.com/JanssenProject/jans@42f2834680de9578bce5f39ac5de35de19a6c48d#egg=jans-pycloudlib&subdirectory=jans-pycloudlib diff --git a/docker-jans-persistence-loader/Dockerfile b/docker-jans-persistence-loader/Dockerfile index 47e4b2b287a..73087e6798f 100644 --- a/docker-jans-persistence-loader/Dockerfile +++ b/docker-jans-persistence-loader/Dockerfile @@ -52,7 +52,7 @@ RUN cd /tmp/jans \ && cp -R ${JANS_SCRIPT_CATALOG_DIR} /app/script-catalog \ && cp ${JANS_SETUP_DIR}/static/extension/introspection/introspection_role_based_scope.py /app/openbanking/static/extension/introspection/ -RUN mkdir -p /app/templates/jans-config-api +RUN mkdir -p /app/templates # partially sync templates from linux-setup RUN cd /tmp/jans \ @@ -64,7 +64,6 @@ RUN cd /tmp/jans \ && cp ${JANS_SETUP_DIR}/static/cache-refresh/o_site.ldif /app/templates/o_site.ldif \ && cp -R ${JANS_SETUP_DIR}/templates/jans-fido2 /app/templates/jans-fido2 \ && cp -R ${JANS_SETUP_DIR}/templates/jans-scim /app/templates/jans-scim \ - && cp ${JANS_SETUP_DIR}/templates/jans-config-api/config.ldif /app/templates/jans-config-api/config.ldif \ && cp -R ${JANS_SETUP_DIR}/templates/jans-cli /app/templates/jans-cli # Download jans-config-api-swagger for role_scope_mapping @@ -166,9 +165,7 @@ ENV CN_CACHE_TYPE=NATIVE_PERSISTENCE \ CN_JACKRABBIT_ADMIN_ID_FILE=/etc/jans/conf/jackrabbit_admin_id \ CN_JACKRABBIT_ADMIN_PASSWORD_FILE=/etc/jans/conf/jackrabbit_admin_password \ GOOGLE_PROJECT_ID="" \ - GOOGLE_APPLICATION_CREDENTIALS=/etc/jans/conf/google-credentials.json \ - CN_AUTH_SERVER_URL="" \ - CN_TOKEN_SERVER_BASE_HOSTNAME="" + GOOGLE_APPLICATION_CREDENTIALS=/etc/jans/conf/google-credentials.json # ==== # misc diff --git a/docker-jans-persistence-loader/README.md b/docker-jans-persistence-loader/README.md index fe262c49954..baf66bbbbfe 100644 --- a/docker-jans-persistence-loader/README.md +++ b/docker-jans-persistence-loader/README.md @@ -70,8 +70,6 @@ The following environment variables are supported by the container: - `GOOGLE_APPLICATION_CREDENTIALS`: Path to Google credentials JSON file (default to `/etc/jans/conf/google-credentials.json`). Used when `CN_CONFIG_ADAPTER` or `CN_SECRET_ADAPTER` set to `google`. - `CN_GOOGLE_SPANNER_INSTANCE_ID`: Google Spanner instance ID. - `CN_GOOGLE_SPANNER_DATABASE_ID`: Google Spanner database ID. -- `CN_AUTH_SERVER_URL`: Base URL of Janssen Auth server, i.e. `auth-server:8080` (default to empty string). -- `CN_TOKEN_SERVER_BASE_HOSTNAME`: Hostname of token server (default to empty string). - `CN_SQL_DB_HOST`: Hostname of the SQL database (default to `localhost`). - `CN_SQL_DB_PORT`: Port of the SQL database (default to `3306` for MySQL). - `CN_SQL_DB_NAME`: SQL database name (default to `jans`). diff --git a/docker-jans-persistence-loader/requirements.txt b/docker-jans-persistence-loader/requirements.txt index bf30ca0c355..ba680ac387d 100644 --- a/docker-jans-persistence-loader/requirements.txt +++ b/docker-jans-persistence-loader/requirements.txt @@ -2,4 +2,4 @@ grpcio==1.41.0 libcst<0.4 ruamel.yaml==0.16.10 -git+https://github.com/JanssenProject/jans@9b536ab2b5d398a41733790f2eeb70339f993fb7#egg=jans-pycloudlib&subdirectory=jans-pycloudlib +git+https://github.com/JanssenProject/jans@42f2834680de9578bce5f39ac5de35de19a6c48d#egg=jans-pycloudlib&subdirectory=jans-pycloudlib diff --git a/docker-jans-persistence-loader/scripts/ldap_setup.py b/docker-jans-persistence-loader/scripts/ldap_setup.py index 4f3cea79860..2e610ced651 100644 --- a/docker-jans-persistence-loader/scripts/ldap_setup.py +++ b/docker-jans-persistence-loader/scripts/ldap_setup.py @@ -7,7 +7,7 @@ from ldap3.core.exceptions import LDAPSocketOpenError from jans.pycloudlib.persistence.ldap import LdapClient -from jans.pycloudlib.persistence.utils import PersistenceMapper +# from jans.pycloudlib.persistence.utils import PersistenceMapper from settings import LOGGING_CONFIG from utils import prepare_template_ctx diff --git a/docker-jans-persistence-loader/scripts/spanner_setup.py b/docker-jans-persistence-loader/scripts/spanner_setup.py index 01af6962518..382dfddaa81 100644 --- a/docker-jans-persistence-loader/scripts/spanner_setup.py +++ b/docker-jans-persistence-loader/scripts/spanner_setup.py @@ -357,6 +357,11 @@ def column_int_to_string(table_name, col_name): ("jansUmaPCT", "dpop"), ("jansClnt", "o"), ("jansClnt", "jansGrp"), + ("jansScope", "creatorId"), + ("jansScope", "creatorTyp"), + ("jansScope", "creatorAttrs"), + ("jansScope", "creationDate"), + ("jansStatEntry", "jansData"), ]: add_column(mod[0], mod[1]) @@ -395,6 +400,13 @@ def column_int_to_string(table_name, col_name): ("jansUmaResource", "jansUmaScope"), ("jansU2fReq", "jansReq"), ("jansFido2AuthnEntry", "jansAuthData"), + ("agmFlowRun", "agFlowEncCont"), + ("agmFlowRun", "agFlowSt"), + ("agmFlowRun", "jansCustomMessage"), + ("agmFlow", "agFlowMeta"), + ("agmFlow", "agFlowTrans"), + ("agmFlow", "jansCustomMessage"), + ("jansOrganization", "jansCustomMessage"), ]: change_column_type(mod[0], mod[1]) diff --git a/docker-jans-persistence-loader/scripts/sql_setup.py b/docker-jans-persistence-loader/scripts/sql_setup.py index cfd899b6cd2..a766192fb33 100644 --- a/docker-jans-persistence-loader/scripts/sql_setup.py +++ b/docker-jans-persistence-loader/scripts/sql_setup.py @@ -362,6 +362,11 @@ def column_from_json(table_name, col_name): ("jansUmaPCT", "dpop"), ("jansClnt", "o"), ("jansClnt", "jansGrp"), + ("jansScope", "creatorId"), + ("jansScope", "creatorTyp"), + ("jansScope", "creatorAttrs"), + ("jansScope", "creationDate"), + ("jansStatEntry", "jansData"), ]: add_column(mod[0], mod[1]) @@ -401,6 +406,13 @@ def column_from_json(table_name, col_name): ("jansU2fReq", "jansReq"), ("jansFido2AuthnEntry", "jansAuthData"), ("jansFido2RegistrationEntry", "jansCodeChallengeHash"), + ("agmFlowRun", "agFlowEncCont"), + ("agmFlowRun", "agFlowSt"), + ("agmFlowRun", "jansCustomMessage"), + ("agmFlow", "agFlowMeta"), + ("agmFlow", "agFlowTrans"), + ("agmFlow", "jansCustomMessage"), + ("jansOrganization", "jansCustomMessage"), ]: change_column_type(mod[0], mod[1]) diff --git a/docker-jans-persistence-loader/scripts/upgrade.py b/docker-jans-persistence-loader/scripts/upgrade.py index ddae07daba9..1b3ddc3677a 100644 --- a/docker-jans-persistence-loader/scripts/upgrade.py +++ b/docker-jans-persistence-loader/scripts/upgrade.py @@ -447,7 +447,6 @@ def invoke(self): self.update_attributes_entries() self.update_scripts_entries() self.update_admin_ui_config() - self.update_api_dynamic_config() def update_scripts_entries(self): # default to ldap persistence @@ -668,54 +667,6 @@ def update_people_entries(self): self.user_backend.modify_entry(entry.id, entry.attrs, **kwargs) def update_clients_entries(self): - # modify redirect UI of config-api client - def _update_jca_client(): - kwargs = {} - jca_client_id = self.manager.config.get("jca_client_id") - id_ = f"inum={jca_client_id},ou=clients,o=jans" - - if self.backend.type in ("sql", "spanner"): - kwargs = {"table_name": "jansClnt"} - id_ = doc_id_from_dn(id_) - elif self.backend.type == "couchbase": - kwargs = {"bucket": os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans")} - id_ = id_from_dn(id_) - - entry = self.backend.get_entry(id_, **kwargs) - - if not entry: - return - - should_update = False - - hostname = self.manager.config.get("hostname") - scopes = [JANS_SCIM_USERS_READ_SCOPE_DN, JANS_SCIM_USERS_WRITE_SCOPE_DN, JANS_STAT_SCOPE_DN] - - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": - if f"https://{hostname}/admin" not in entry.attrs["jansRedirectURI"]["v"]: - entry.attrs["jansRedirectURI"]["v"].append(f"https://{hostname}/admin") - should_update = True - - # add jans_stat, SCIM users.read, SCIM users.write scopes to config-api client - for scope in scopes: - if scope not in entry.attrs["jansScope"]["v"]: - entry.attrs["jansScope"]["v"].append(scope) - should_update = True - - else: # ldap, couchbase, and spanner - if f"https://{hostname}/admin" not in entry.attrs["jansRedirectURI"]: - entry.attrs["jansRedirectURI"].append(f"https://{hostname}/admin") - should_update = True - - # add jans_stat, SCIM users.read, SCIM users.write scopes to config-api client - for scope in scopes: - if scope not in entry.attrs["jansScope"]: - entry.attrs["jansScope"].append(scope) - should_update = True - - if should_update: - self.backend.modify_entry(entry.id, entry.attrs, **kwargs) - # modify introspection script for token server client def _update_token_server_client(): kwargs = {} @@ -744,7 +695,6 @@ def _update_token_server_client(): entry.attrs["jansAttrs"] = json.dumps(attrs) self.backend.modify_entry(entry.id, entry.attrs, **kwargs) - _update_jca_client() _update_token_server_client() def update_admin_ui_config(self): @@ -819,34 +769,6 @@ def update_admin_ui_config(self): entry.attrs["jansRevision"] += 1 self.backend.modify_entry(entry.id, entry.attrs, **kwargs) - def update_api_dynamic_config(self): - kwargs = {} - id_ = "ou=jans-config-api,ou=configuration,o=jans" - - if self.backend.type in ("sql", "spanner"): - kwargs = {"table_name": "jansAppConf"} - id_ = doc_id_from_dn(id_) - elif self.backend.type == "couchbase": - kwargs = {"bucket": os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans")} - id_ = id_from_dn(id_) - - entry = self.backend.get_entry(id_, **kwargs) - - if not entry: - return - - if self.backend.type != "couchbase": - entry.attrs["jansConfDyn"] = json.loads(entry.attrs["jansConfDyn"]) - - conf, should_update = _transform_api_dynamic_config(entry.attrs["jansConfDyn"]) - - if should_update: - if self.backend.type != "couchbase": - entry.attrs["jansConfDyn"] = json.dumps(conf) - - entry.attrs["jansRevision"] += 1 - self.backend.modify_entry(entry.id, entry.attrs, **kwargs) - def update_auth_errors_config(self): # default to ldap persistence kwargs = {} @@ -906,38 +828,6 @@ def update_auth_static_config(self): self.backend.modify_entry(entry.id, entry.attrs, **kwargs) -def _transform_api_dynamic_config(conf): - should_update = False - - if "userExclusionAttributes" not in conf: - conf["userExclusionAttributes"] = ["userPassword"] - should_update = True - - if "userMandatoryAttributes" not in conf: - conf["userMandatoryAttributes"] = [ - "mail", - "displayName", - "jansStatus", - "userPassword", - "givenName", - ] - should_update = True - - if "agamaConfiguration" not in conf: - conf["agamaConfiguration"] = { - "mandatoryAttributes": [ - "qname", - "source", - ], - "optionalAttributes": [ - "serialVersionUID", - "enabled", - ], - } - should_update = True - return conf, should_update - - def _transform_auth_errors_config(conf): should_update = False diff --git a/docker-jans-persistence-loader/scripts/utils.py b/docker-jans-persistence-loader/scripts/utils.py index 23e2e4f8f64..ef54bd882dc 100644 --- a/docker-jans-persistence-loader/scripts/utils.py +++ b/docker-jans-persistence-loader/scripts/utils.py @@ -1,11 +1,9 @@ -import base64 import contextlib import json import os import typing as _t from itertools import chain from pathlib import Path -from urllib.parse import urlparse from uuid import uuid4 import ruamel.yaml @@ -200,63 +198,6 @@ def merge_auth_ctx(ctx): return ctx -def merge_config_api_ctx(ctx): - def transform_url(url): - auth_server_url = os.environ.get("CN_AUTH_SERVER_URL", "") - - if not auth_server_url: - return url - - parse_result = urlparse(url) - if parse_result.path.startswith("/.well-known"): - path = f"/jans-auth{parse_result.path}" - else: - path = parse_result.path - url = f"http://{auth_server_url}{path}" - return url - - def get_injected_urls(): - auth_config = json.loads( - base64.b64decode(ctx["auth_config_base64"]).decode() - ) - urls = ( - "issuer", - "openIdConfigurationEndpoint", - "introspectionEndpoint", - "tokenEndpoint", - "tokenRevocationEndpoint", - ) - return { - url: transform_url(auth_config[url]) - for url in urls - } - - approved_issuer = [ctx["hostname"]] - token_server_hostname = os.environ.get("CN_TOKEN_SERVER_BASE_HOSTNAME") - if token_server_hostname and token_server_hostname not in approved_issuer: - approved_issuer.append(token_server_hostname) - - local_ctx = { - "apiApprovedIssuer": ",".join([f'"https://{issuer}"' for issuer in approved_issuer]), - "apiProtectionType": "oauth2", - "jca_client_id": ctx["jca_client_id"], - "jca_client_encoded_pw": ctx["jca_client_encoded_pw"], - "endpointInjectionEnabled": "true", - "configOauthEnabled": str(os.environ.get("CN_CONFIG_API_OAUTH_ENABLED") or True).lower(), - } - local_ctx.update(get_injected_urls()) - - basedir = '/app/templates/jans-config-api' - file_mappings = { - "config_api_dynamic_conf_base64": "dynamic-conf.json", - } - for key, file_ in file_mappings.items(): - file_path = os.path.join(basedir, file_) - with open(file_path) as fp: - ctx[key] = generate_base64_contents(fp.read() % local_ctx) - return ctx - - def merge_jans_cli_ctx(manager, ctx): # WARNING: # - deprecated configs and secrets for role_based @@ -287,7 +228,6 @@ def prepare_template_ctx(manager): ctx = get_base_ctx(manager) ctx = merge_extension_ctx(ctx) ctx = merge_auth_ctx(ctx) - ctx = merge_config_api_ctx(ctx) ctx = merge_jans_cli_ctx(manager, ctx) return ctx @@ -301,7 +241,6 @@ def get_ldif_mappings(group, optional_scopes=None): def default_files(): files = [ "base.ldif", - "jans-config-api/scopes.ldif", ] if dist == "openbanking": @@ -310,7 +249,6 @@ def default_files(): "scopes.ob.ldif", "scripts.ob.ldif", "configuration.ob.ldif", - "jans-config-api/clients.ob.ldif", ] else: files += [ @@ -319,12 +257,10 @@ def default_files(): "scripts.ldif", "configuration.ldif", "o_metric.ldif", - "jans-config-api/clients.ldif", "agama.ldif", ] files += [ - "jans-config-api/config.ldif", "jans-auth/configuration.ldif", "jans-auth/role-scope-mappings.ldif", "jans-cli/client.ldif", diff --git a/docker-jans-persistence-loader/templates/jans-config-api/clients.ldif b/docker-jans-persistence-loader/templates/jans-config-api/clients.ldif deleted file mode 100644 index bea8941b32e..00000000000 --- a/docker-jans-persistence-loader/templates/jans-config-api/clients.ldif +++ /dev/null @@ -1,72 +0,0 @@ -dn: inum=%(jca_client_id)s,ou=clients,o=jans -del: false -displayName: Jans Config Api Client -inum: %(jca_client_id)s -jansAccessTknAsJwt: false -jansAccessTknSigAlg: RS256 -jansAppTyp: web -jansAttrs: {"tlsClientAuthSubjectDn": "", "runIntrospectionScriptBeforeJwtCreation": false, "keepClientAuthorizationAfterExpiration": false, "allowSpontaneousScopes": false, "spontaneousScopes": [], "spontaneousScopeScriptDns": [], "backchannelLogoutUri": [], "backchannelLogoutSessionRequired": false, "additionalAudience": [], "postAuthnScripts": [], "consentGatheringScripts": [], "introspectionScripts": [], "rptClaimsScripts": []} -jansClntSecret: %(jca_client_encoded_pw)s -jansDisabled: false -jansGrantTyp: authorization_code -jansGrantTyp: refresh_token -jansGrantTyp: client_credentials -jansIdTknSignedRespAlg: RS256 -jansInclClaimsInIdTkn: false -jansLogoutSessRequired: false -jansPersistClntAuthzs: true -jansRespTyp: code -jansRptAsJwt: false -jansScope: inum=1800.4F4C08,ou=scopes,o=jans -jansScope: inum=1800.61D3E9,ou=scopes,o=jans -jansScope: inum=1800.78D299,ou=scopes,o=jans -jansScope: inum=1800.C38990,ou=scopes,o=jans -jansScope: inum=1800.13AA0E,ou=scopes,o=jans -jansScope: inum=1800.A01874,ou=scopes,o=jans -jansScope: inum=1800.36FC16,ou=scopes,o=jans -jansScope: inum=1800.0D9CCC,ou=scopes,o=jans -jansScope: inum=1800.B8DE82,ou=scopes,o=jans -jansScope: inum=1800.42C38F,ou=scopes,o=jans -jansScope: inum=1800.10F720,ou=scopes,o=jans -jansScope: inum=1800.F4E351,ou=scopes,o=jans -jansScope: inum=1800.3263EF,ou=scopes,o=jans -jansScope: inum=1800.B0C433,ou=scopes,o=jans -jansScope: inum=1800.419DD5,ou=scopes,o=jans -jansScope: inum=1800.158007,ou=scopes,o=jans -jansScope: inum=1800.671341,ou=scopes,o=jans -jansScope: inum=1800.79932F,ou=scopes,o=jans -jansScope: inum=1800.45C56E,ou=scopes,o=jans -jansScope: inum=1800.F815D0,ou=scopes,o=jans -jansScope: inum=1800.72FC9F,ou=scopes,o=jans -jansScope: inum=1800.D2E431,ou=scopes,o=jans -jansScope: inum=1800.05CA71,ou=scopes,o=jans -jansScope: inum=1800.CAA614,ou=scopes,o=jans -jansScope: inum=1800.4B522D,ou=scopes,o=jans -jansScope: inum=1800.28FF8B,ou=scopes,o=jans -jansScope: inum=1800.07C227,ou=scopes,o=jans -jansScope: inum=1800.9D4EBE,ou=scopes,o=jans -jansScope: inum=1800.3E6BA7,ou=scopes,o=jans -jansScope: inum=1800.FE975D,ou=scopes,o=jans -jansScope: inum=1800.C0B661,ou=scopes,o=jans -jansScope: inum=1800.7FD3C9,ou=scopes,o=jans -jansScope: inum=1800.DCE0C3,ou=scopes,o=jans -jansScope: inum=1800.BDCE9B,ou=scopes,o=jans -jansScope: inum=1800.33641E,ou=scopes,o=jans -jansScope: inum=1800.B15085,ou=scopes,o=jans -jansScope: inum=1800.FB7583,ou=scopes,o=jans -jansScope: inum=1800.A524C2,ou=scopes,o=jans -jansScope: inum=1800.23C17E,ou=scopes,o=jans -jansScope: inum=1800.BC5317,ou=scopes,o=jans -jansScope: inum=F0C4,ou=scopes,o=jans -jansScope: inum=764C,ou=scopes,o=jans -jansScope: inum=10B2,ou=scopes,o=jans -jansScope: inum=C4F7,ou=scopes,o=jans -jansScope: inum=1200.2B7428,ou=scopes,o=jans -jansScope: inum=1200.0A0198,ou=scopes,o=jans -jansSubjectTyp: pairwise -jansTknEndpointAuthMethod: client_secret_basic -jansTrustedClnt: false -objectClass: top -objectClass: jansClnt -jansRedirectURI: https://%(hostname)s/admin -jansRedirectURI: http://localhost:4100 diff --git a/docker-jans-persistence-loader/templates/jans-config-api/clients.ob.ldif b/docker-jans-persistence-loader/templates/jans-config-api/clients.ob.ldif deleted file mode 100644 index 3dd36a97cd0..00000000000 --- a/docker-jans-persistence-loader/templates/jans-config-api/clients.ob.ldif +++ /dev/null @@ -1,66 +0,0 @@ -dn: inum=%(jca_client_id)s,ou=clients,o=jans -del: false -displayName: Jans Config Api Client -inum: %(jca_client_id)s -jansAccessTknAsJwt: false -jansAccessTknSigAlg: RS256 -jansAppTyp: web -jansAttrs: {"tlsClientAuthSubjectDn": "", "runIntrospectionScriptBeforeJwtCreation": false, "keepClientAuthorizationAfterExpiration": false, "allowSpontaneousScopes": false, "spontaneousScopes": [], "spontaneousScopeScriptDns": [], "backchannelLogoutUri": [], "backchannelLogoutSessionRequired": false, "additionalAudience": [], "postAuthnScripts": [], "consentGatheringScripts": [], "introspectionScripts": [], "rptClaimsScripts": []} -jansClntSecret: %(jca_client_encoded_pw)s -jansDisabled: false -jansGrantTyp: authorization_code -jansGrantTyp: refresh_token -jansGrantTyp: client_credentials -jansIdTknSignedRespAlg: RS256 -jansInclClaimsInIdTkn: false -jansLogoutSessRequired: false -jansPersistClntAuthzs: true -jansRespTyp: code -jansRptAsJwt: false -jansScope: inum=1800.4F4C08,ou=scopes,o=jans -jansScope: inum=1800.61D3E9,ou=scopes,o=jans -jansScope: inum=1800.78D299,ou=scopes,o=jans -jansScope: inum=1800.C38990,ou=scopes,o=jans -jansScope: inum=1800.13AA0E,ou=scopes,o=jans -jansScope: inum=1800.A01874,ou=scopes,o=jans -jansScope: inum=1800.36FC16,ou=scopes,o=jans -jansScope: inum=1800.0D9CCC,ou=scopes,o=jans -jansScope: inum=1800.B8DE82,ou=scopes,o=jans -jansScope: inum=1800.42C38F,ou=scopes,o=jans -jansScope: inum=1800.10F720,ou=scopes,o=jans -jansScope: inum=1800.F4E351,ou=scopes,o=jans -jansScope: inum=1800.3263EF,ou=scopes,o=jans -jansScope: inum=1800.B0C433,ou=scopes,o=jans -jansScope: inum=1800.419DD5,ou=scopes,o=jans -jansScope: inum=1800.158007,ou=scopes,o=jans -jansScope: inum=1800.671341,ou=scopes,o=jans -jansScope: inum=1800.79932F,ou=scopes,o=jans -jansScope: inum=1800.45C56E,ou=scopes,o=jans -jansScope: inum=1800.F815D0,ou=scopes,o=jans -jansScope: inum=1800.72FC9F,ou=scopes,o=jans -jansScope: inum=1800.D2E431,ou=scopes,o=jans -jansScope: inum=1800.05CA71,ou=scopes,o=jans -jansScope: inum=1800.CAA614,ou=scopes,o=jans -jansScope: inum=1800.4B522D,ou=scopes,o=jans -jansScope: inum=1800.28FF8B,ou=scopes,o=jans -jansScope: inum=1800.07C227,ou=scopes,o=jans -jansScope: inum=1800.9D4EBE,ou=scopes,o=jans -jansScope: inum=1800.3E6BA7,ou=scopes,o=jans -jansScope: inum=1800.FE975D,ou=scopes,o=jans -jansScope: inum=1800.C0B661,ou=scopes,o=jans -jansScope: inum=1800.7FD3C9,ou=scopes,o=jans -jansScope: inum=1800.DCE0C3,ou=scopes,o=jans -jansScope: inum=1800.BDCE9B,ou=scopes,o=jans -jansScope: inum=1800.33641E,ou=scopes,o=jans -jansScope: inum=1800.B15085,ou=scopes,o=jans -jansScope: inum=1800.FB7583,ou=scopes,o=jans -jansScope: inum=1800.A524C2,ou=scopes,o=jans -jansScope: inum=1800.23C17E,ou=scopes,o=jans -jansScope: inum=1800.BC5317,ou=scopes,o=jans -jansSubjectTyp: pairwise -jansTknEndpointAuthMethod: client_secret_basic -jansTrustedClnt: false -objectClass: top -objectClass: jansClnt -jansRedirectURI: https://%(hostname)s/admin-ui -jansRedirectURI: http://localhost:4100 diff --git a/docker-jans-persistence-loader/templates/jans-config-api/dynamic-conf.json b/docker-jans-persistence-loader/templates/jans-config-api/dynamic-conf.json deleted file mode 100644 index d2f74d03982..00000000000 --- a/docker-jans-persistence-loader/templates/jans-config-api/dynamic-conf.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "configOauthEnabled": %(configOauthEnabled)s, - "apiApprovedIssuer": [%(apiApprovedIssuer)s], - "apiProtectionType": "%(apiProtectionType)s", - "apiClientId": "%(jca_client_id)s", - "apiClientPassword": "%(jca_client_encoded_pw)s", - "endpointInjectionEnabled": %(endpointInjectionEnabled)s, - "authIssuerUrl": "%(issuer)s", - "authOpenidConfigurationUrl": "%(openIdConfigurationEndpoint)s", - "authOpenidIntrospectionUrl": "%(introspectionEndpoint)s", - "authOpenidTokenUrl": "%(tokenEndpoint)s", - "authOpenidRevokeUrl": "%(tokenRevocationEndpoint)s", - "smallryeHealthRootPath": "/health-check", - "disableJdkLogger":true, - "loggingLevel":"INFO", - "loggingLayout":"text", - "externalLoggerConfiguration":"", - "exclusiveAuthScopes": [ - "jans_stat", - "https://jans.io/scim/users.read", - "https://jans.io/scim/users.write" - ], - "corsConfigurationFilters": [ - { - "filterName": "CorsFilter", - "corsAllowedOrigins": "*", - "corsAllowedMethods": "GET,PUT,POST,DELETE,PATCH,HEAD,OPTIONS", - "corsAllowedHeaders": "", - "corsExposedHeaders": "", - "corsSupportCredentials": true, - "corsLoggingEnabled": false, - "corsPreflightMaxAge": 1800, - "corsRequestDecorate": true, - "corsEnabled": true - } - ], - "userExclusionAttributes": [ - "userPassword" - ], - "userMandatoryAttributes": [ - "mail", - "displayName", - "jansStatus", - "userPassword", - "givenName" - ], - "agamaConfiguration": { - "mandatoryAttributes": [ - "qname", - "source" - ], - "optionalAttributes": [ - "serialVersionUID", - "enabled" - ] - } -} diff --git a/docker-jans-persistence-loader/templates/jans-config-api/scopes.ldif b/docker-jans-persistence-loader/templates/jans-config-api/scopes.ldif deleted file mode 100644 index 213725a1b76..00000000000 --- a/docker-jans-persistence-loader/templates/jans-config-api/scopes.ldif +++ /dev/null @@ -1,461 +0,0 @@ -dn: inum=1800.4F4C08,ou=scopes,o=jans -description: View Auth Server properties related information -displayName: Config API scope https://jans.io/oauth/jans-auth-server/config/properties.readonly -inum: 1800.4F4C08 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/jans-auth-server/config/properties.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.61D3E9,ou=scopes,o=jans -description: Manage Auth Server properties related information -displayName: Config API scope https://jans.io/oauth/jans-auth-server/config/properties.write -inum: 1800.61D3E9 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/jans-auth-server/config/properties.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.78D299,ou=scopes,o=jans -description: View FIDO2 related information -displayName: Config API scope https://jans.io/oauth/config/fido2.readonly -inum: 1800.78D299 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/fido2.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.C38990,ou=scopes,o=jans -description: Manage FIDO2 related information -displayName: Config API scope https://jans.io/oauth/config/fido2.write -inum: 1800.C38990 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/fido2.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.13AA0E,ou=scopes,o=jans -description: View attribute related information -displayName: Config API scope https://jans.io/oauth/config/attributes.readonly -inum: 1800.13AA0E -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/attributes.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.A01874,ou=scopes,o=jans -description: Manage attribute related information -displayName: Config API scope https://jans.io/oauth/config/attributes.write -inum: 1800.A01874 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/attributes.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.36FC16,ou=scopes,o=jans -description: Delete attribute related information -displayName: Config API scope https://jans.io/oauth/config/attributes.delete -inum: 1800.36FC16 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/attributes.delete -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.0D9CCC,ou=scopes,o=jans -description: View ACRS related information -displayName: Config API scope https://jans.io/oauth/config/acrs.readonly -inum: 1800.0D9CCC -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/acrs.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.B8DE82,ou=scopes,o=jans -description: Manage ACRS related information -displayName: Config API scope https://jans.io/oauth/config/acrs.write -inum: 1800.B8DE82 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/acrs.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.42C38F,ou=scopes,o=jans -description: View LDAP database related information -displayName: Config API scope https://jans.io/oauth/config/database/ldap.readonly -inum: 1800.42C38F -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/database/ldap.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.10F720,ou=scopes,o=jans -description: Manage LDAP database related information -displayName: Config API scope https://jans.io/oauth/config/database/ldap.write -inum: 1800.10F720 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/database/ldap.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.F4E351,ou=scopes,o=jans -description: Delete LDAP database related information -displayName: Config API scope https://jans.io/oauth/config/database/ldap.delete -inum: 1800.F4E351 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/database/ldap.delete -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.3263EF,ou=scopes,o=jans -description: View Couchbase database information -displayName: Config API scope https://jans.io/oauth/config/database/couchbase.readonly -inum: 1800.3263EF -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/database/couchbase.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.B0C433,ou=scopes,o=jans -description: Manage Couchbase database related information -displayName: Config API scope https://jans.io/oauth/config/database/couchbase.write -inum: 1800.B0C433 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/database/couchbase.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.419DD5,ou=scopes,o=jans -description: Delete Couchbase database related information -displayName: Config API scope https://jans.io/oauth/config/database/couchbase.delete -inum: 1800.419DD5 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/database/couchbase.delete -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.158007,ou=scopes,o=jans -description: View cache scripts information -displayName: Config API scope https://jans.io/oauth/config/scripts.readonly -inum: 1800.158007 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/scripts.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.671341,ou=scopes,o=jans -description: Manage scripts related information -displayName: Config API scope https://jans.io/oauth/config/scripts.write -inum: 1800.671341 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/scripts.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.79932F,ou=scopes,o=jans -description: Delete scripts related information -displayName: Config API scope https://jans.io/oauth/config/scripts.delete -inum: 1800.79932F -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/scripts.delete -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.45C56E,ou=scopes,o=jans -description: View cache related information -displayName: Config API scope https://jans.io/oauth/config/cache.readonly -inum: 1800.45C56E -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/cache.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.F815D0,ou=scopes,o=jans -description: Manage cache related information -displayName: Config API scope https://jans.io/oauth/config/cache.write -inum: 1800.F815D0 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/cache.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.72FC9F,ou=scopes,o=jans -description: View SMTP related information -displayName: Config API scope https://jans.io/oauth/config/smtp.readonly -inum: 1800.72FC9F -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/smtp.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.D2E431,ou=scopes,o=jans -description: Manage SMTP related information -displayName: Config API scope https://jans.io/oauth/config/smtp.write -inum: 1800.D2E431 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/smtp.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.05CA71,ou=scopes,o=jans -description: Delete SMTP related information -displayName: Config API scope https://jans.io/oauth/config/smtp.delete -inum: 1800.05CA71 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/smtp.delete -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.CAA614,ou=scopes,o=jans -description: View logging related information -displayName: Config API scope https://jans.io/oauth/config/logging.readonly -inum: 1800.CAA614 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/logging.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.4B522D,ou=scopes,o=jans -description: Manage logging related information -displayName: Config API scope https://jans.io/oauth/config/logging.write -inum: 1800.4B522D -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/logging.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.28FF8B,ou=scopes,o=jans -description: View JWKS related information -displayName: Config API scope https://jans.io/oauth/config/jwks.readonly -inum: 1800.28FF8B -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/jwks.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.07C227,ou=scopes,o=jans -description: Manage JWKS related information -displayName: Config API scope https://jans.io/oauth/config/jwks.write -inum: 1800.07C227 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/jwks.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.9D4EBE,ou=scopes,o=jans -description: View clients related information -displayName: Config API scope https://jans.io/oauth/config/openid/clients.readonly -inum: 1800.9D4EBE -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/openid/clients.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.3E6BA7,ou=scopes,o=jans -description: Manage clients related information -displayName: Config API scope https://jans.io/oauth/config/openid/clients.write -inum: 1800.3E6BA7 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/openid/clients.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.FE975D,ou=scopes,o=jans -description: Delete clients related information -displayName: Config API scope https://jans.io/oauth/config/openid/clients.delete -inum: 1800.FE975D -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/openid/clients.delete -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.C0B661,ou=scopes,o=jans -description: View scope related information -displayName: Config API scope https://jans.io/oauth/config/scopes.readonly -inum: 1800.C0B661 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/scopes.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.7FD3C9,ou=scopes,o=jans -description: Manage scope related information -displayName: Config API scope https://jans.io/oauth/config/scopes.write -inum: 1800.7FD3C9 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/scopes.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.DCE0C3,ou=scopes,o=jans -description: Delete scope related information -displayName: Config API scope https://jans.io/oauth/config/scopes.delete -inum: 1800.DCE0C3 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/scopes.delete -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.BDCE9B,ou=scopes,o=jans -description: View UMA Resource related information -displayName: Config API scope https://jans.io/oauth/config/uma/resources.readonly -inum: 1800.BDCE9B -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/uma/resources.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.33641E,ou=scopes,o=jans -description: Manage UMA Resource related information -displayName: Config API scope https://jans.io/oauth/config/uma/resources.write -inum: 1800.33641E -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/uma/resources.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.B15085,ou=scopes,o=jans -description: Delete UMA Resource related information -displayName: Config API scope https://jans.io/oauth/config/uma/resources.delete -inum: 1800.B15085 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/uma/resources.delete -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.FB7583,ou=scopes,o=jans -description: View SQL database related information -displayName: Config API scope https://jans.io/oauth/config/database/sql.readonly -inum: 1800.FB7583 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/database/sql.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.A524C2,ou=scopes,o=jans -description: Manage SQL database related information -displayName: Config API scope https://jans.io/oauth/config/database/sql.write -inum: 1800.A524C2 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/database/sql.write -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.23C17E,ou=scopes,o=jans -description: Delete SQL database related information -displayName: Config API scope https://jans.io/oauth/config/database/sql.delete -inum: 1800.23C17E -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/database/sql.delete -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1800.BC5317,ou=scopes,o=jans -description: Vew server with basic statistic -displayName: Config API scope https://jans.io/oauth/config/stats.readonly -inum: 1800.BC5317 -jansAttrs: {"spontaneousClientId": null, "spontaneousClientScopes": null, "showInConfigurationEndpoint": false} -jansDefScope: false -jansId: https://jans.io/oauth/config/stats.readonly -jansScopeTyp: oauth2 -objectClass: top -objectClass: jansScope - -dn: inum=1200.2B7428,ou=scopes,o=jans -description: Query user resources -displayName: SCIM https://jans.io/scim/users.read -inum: 1200.2B7428 -jansDefScope: false -jansId: https://jans.io/scim/users.read -jansScopeTyp: oauth -objectClass: top -objectClass: jansScope -jansAttrs: {"spontaneousClientId":null,"spontaneousClientScopes":null,"showInConfigurationEndpoint":true} - -dn: inum=1200.0A0198,ou=scopes,o=jans -description: Modify user resources -displayName: SCIM https://jans.io/scim/users.write -inum: 1200.0A0198 -jansDefScope: false -jansId: https://jans.io/scim/users.write -jansScopeTyp: oauth -objectClass: top -objectClass: jansScope -jansAttrs: {"spontaneousClientId":null,"spontaneousClientScopes":null,"showInConfigurationEndpoint":true} diff --git a/docker-jans-scim/Dockerfile b/docker-jans-scim/Dockerfile index 24518751296..b8445ac6b9c 100644 --- a/docker-jans-scim/Dockerfile +++ b/docker-jans-scim/Dockerfile @@ -46,7 +46,7 @@ RUN wget -q https://maven.jans.io/maven/io/jans/jython-installer/${JYTHON_VERSIO # ==== ENV CN_VERSION=1.0.3-SNAPSHOT -ENV CN_BUILD_DATE='2022-10-14 18:41' +ENV CN_BUILD_DATE='2022-10-24 13:32' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-scim-server/${CN_VERSION}/jans-scim-server-${CN_VERSION}.war # Install SCIM @@ -60,12 +60,33 @@ RUN mkdir -p ${JETTY_BASE}/jans-scim/webapps \ && java -jar ${JETTY_HOME}/start.jar jetty.home=${JETTY_HOME} jetty.base=${JETTY_BASE}/jans-scim --add-to-start=server,deploy,resources,http,http-forwarded,jsp,websocket,cdi-decorate \ && rm -rf /tmp/jans-scim.war /tmp/WEB-INF +# ====== +# Python +# ====== + +COPY requirements.txt /app/requirements.txt +RUN python3 -m ensurepip \ + && pip3 install --no-cache-dir -U pip wheel \ + && pip3 install --no-cache-dir -r /app/requirements.txt \ + && pip3 uninstall -y pip wheel + +# ========== +# Prometheus +# ========== + +ARG PROMETHEUS_JAVAAGENT_VERSION=0.17.2 +COPY conf/prometheus-config.yaml /opt/prometheus/ +RUN mkdir -p /opt/prometheus \ + && wget -q https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/${PROMETHEUS_JAVAAGENT_VERSION}/jmx_prometheus_javaagent-${PROMETHEUS_JAVAAGENT_VERSION}.jar -O /opt/prometheus/jmx_prometheus_javaagent.jar \ + && java -jar ${JETTY_HOME}/start.jar jetty.home=${JETTY_HOME} jetty.base=${JETTY_BASE}/jans-scim --add-module=jmx,stats + # ===================== # jans-linux-setup sync # ===================== ENV JANS_SOURCE_VERSION=e74ea8e27e59d35ff6e3c6f997e6c1df6a04ec83 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup +ARG JANS_SCIM_RESOURCE_DIR=jans-scim/server/src/main/resources # note that as we're pulling from a monorepo (with multiple project in it) # we are using partial-clone and sparse-checkout to get the jans-linux-setup code @@ -73,7 +94,8 @@ RUN git clone --filter blob:none --no-checkout https://github.com/janssenproject && cd /tmp/jans \ && git sparse-checkout init --cone \ && git checkout ${JANS_SOURCE_VERSION} \ - && git sparse-checkout set ${JANS_SETUP_DIR} + && git sparse-checkout add ${JANS_SETUP_DIR} \ + && git sparse-checkout add ${JANS_SCIM_RESOURCE_DIR} RUN mkdir -p /app/static/rdbm \ /app/schema \ @@ -90,31 +112,12 @@ RUN cd /tmp/jans \ && cp ${JANS_SETUP_DIR}/schema/opendj_types.json /app/schema/ \ && cp ${JANS_SETUP_DIR}/templates/jans-scim/configuration.ldif /app/templates/jans-scim/ \ && cp ${JANS_SETUP_DIR}/templates/jans-scim/dynamic-conf.json /app/templates/jans-scim/ \ - && cp ${JANS_SETUP_DIR}/templates/jans-scim/static-conf.json /app/templates/jans-scim/ + && cp ${JANS_SETUP_DIR}/templates/jans-scim/static-conf.json /app/templates/jans-scim/ \ + && cp ${JANS_SCIM_RESOURCE_DIR}/jans-scim-openapi.yaml /app/static/ # cleanup RUN rm -rf /tmp/jans -# ====== -# Python -# ====== - -COPY requirements.txt /app/requirements.txt -RUN python3 -m ensurepip \ - && pip3 install --no-cache-dir -U pip wheel \ - && pip3 install --no-cache-dir -r /app/requirements.txt \ - && pip3 uninstall -y pip wheel - -# ========== -# Prometheus -# ========== - -ARG PROMETHEUS_JAVAAGENT_VERSION=0.17.2 -COPY conf/prometheus-config.yaml /opt/prometheus/ -RUN mkdir -p /opt/prometheus \ - && wget -q https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/${PROMETHEUS_JAVAAGENT_VERSION}/jmx_prometheus_javaagent-${PROMETHEUS_JAVAAGENT_VERSION}.jar -O /opt/prometheus/jmx_prometheus_javaagent.jar \ - && java -jar ${JETTY_HOME}/start.jar jetty.home=${JETTY_HOME} jetty.base=${JETTY_BASE}/jans-scim --add-module=jmx,stats - # ======= # Cleanup # ======= diff --git a/docker-jans-scim/requirements.txt b/docker-jans-scim/requirements.txt index 195561e867d..ba680ac387d 100644 --- a/docker-jans-scim/requirements.txt +++ b/docker-jans-scim/requirements.txt @@ -1,4 +1,5 @@ # pinned to py3-grpcio version to avoid failure on native extension build grpcio==1.41.0 libcst<0.4 -git+https://github.com/JanssenProject/jans@9b536ab2b5d398a41733790f2eeb70339f993fb7#egg=jans-pycloudlib&subdirectory=jans-pycloudlib +ruamel.yaml==0.16.10 +git+https://github.com/JanssenProject/jans@42f2834680de9578bce5f39ac5de35de19a6c48d#egg=jans-pycloudlib&subdirectory=jans-pycloudlib diff --git a/docker-jans-scim/scripts/bootstrap.py b/docker-jans-scim/scripts/bootstrap.py index 41108f80729..5f33c104983 100644 --- a/docker-jans-scim/scripts/bootstrap.py +++ b/docker-jans-scim/scripts/bootstrap.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import logging.config import os import re import typing as _t @@ -8,6 +9,8 @@ from string import Template from uuid import uuid4 +from ldif import LDIFWriter + from jans.pycloudlib import get_manager from jans.pycloudlib.persistence import render_couchbase_properties from jans.pycloudlib.persistence import render_base_properties @@ -28,8 +31,8 @@ from jans.pycloudlib.utils import generate_base64_contents from jans.pycloudlib.utils import get_random_chars -import logging.config from settings import LOGGING_CONFIG +from utils import parse_swagger_file if _t.TYPE_CHECKING: # pragma: no cover # imported objects for function type hint, completion, etc. @@ -273,10 +276,71 @@ def ldif_files(self) -> list[str]: return files def import_ldif_files(self) -> None: + self.generate_scopes_ldif() + for file_ in self.ldif_files: logger.info(f"Importing {file_}") self.client.create_from_ldif(file_, self.ctx) + def get_scope_jans_ids(self): + if self.persistence_type in ("sql", "spanner"): + entries = self.client.search("jansScope", ["jansId"]) + return [entry["jansId"] for entry in entries] + + if self.persistence_type == "couchbase": + bucket = os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans") + req = self.client.exec_query( + f"SELECT {bucket}.jansId FROM {bucket} WHERE objectClass = 'jansScope'", + ) + results = req.json()["results"] + return [item["jansId"] for item in results] + + # likely ldap + entries = self.client.search("ou=scopes,o=jans", "(objectClass=jansScope)", ["jansId"]) + return [entry.entry_attributes_as_dict["jansId"][0] for entry in entries] + + def generate_scopes_ldif(self): + # jansId to compare to + existing_jans_ids = self.get_scope_jans_ids() + + def generate_scim_scopes(): + swagger = parse_swagger_file() + scopes = swagger["components"]["securitySchemes"]["scim_oauth"]["flows"]["clientCredentials"]["scopes"] + + generated_scopes = [] + for jans_id, desc in scopes.items(): + if jans_id in existing_jans_ids: + continue + + inum = f"1200.{generate_hex()}-{generate_hex()}" + attrs = { + "description": [desc], + "displayName": [f"SCIM scope {jans_id}"], + "inum": [inum], + "jansAttrs": [json.dumps({"spontaneousClientScopes": None, "showInConfigurationEndpoint": True})], + "jansId": [jans_id], + "jansScopeTyp": ["oauth"], + "objectClass": ["top", "jansScope"], + "jansDefScope": ["false"], + } + generated_scopes.append(attrs) + return generated_scopes + + # prepare required scopes (if any) + scopes = [] + + scim_scopes = generate_scim_scopes() + scopes += scim_scopes + + with open("/app/templates/jans-scim/scopes.ldif", "wb") as fd: + writer = LDIFWriter(fd, cols=1000) + for scope in scopes: + writer.unparse(f"inum={scope['inum'][0]},ou=scopes,o=jans", scope) + + +def generate_hex(size: int = 3): + return os.urandom(size).hex().upper() + if __name__ == "__main__": main() diff --git a/docker-jans-scim/scripts/entrypoint.sh b/docker-jans-scim/scripts/entrypoint.sh index ec0cdab3f31..8204ef6bb5b 100644 --- a/docker-jans-scim/scripts/entrypoint.sh +++ b/docker-jans-scim/scripts/entrypoint.sh @@ -15,6 +15,7 @@ get_prometheus_opt() { python3 /app/scripts/wait.py python3 /app/scripts/bootstrap.py +python3 /app/scripts/upgrade.py cd /opt/jans/jetty/jans-scim exec java \ diff --git a/docker-jans-scim/scripts/upgrade.py b/docker-jans-scim/scripts/upgrade.py new file mode 100644 index 00000000000..887c463db4d --- /dev/null +++ b/docker-jans-scim/scripts/upgrade.py @@ -0,0 +1,290 @@ +import json +import logging.config +import os +from collections import namedtuple + +from jans.pycloudlib import get_manager +from jans.pycloudlib.persistence import CouchbaseClient +from jans.pycloudlib.persistence import LdapClient +from jans.pycloudlib.persistence import SpannerClient +from jans.pycloudlib.persistence import SqlClient +from jans.pycloudlib.persistence import PersistenceMapper +from jans.pycloudlib.persistence import doc_id_from_dn +from jans.pycloudlib.persistence import id_from_dn + +from settings import LOGGING_CONFIG +from utils import parse_swagger_file + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("entrypoint") + +Entry = namedtuple("Entry", ["id", "attrs"]) + + +class LDAPBackend: + def __init__(self, manager): + self.manager = manager + self.client = LdapClient(manager) + self.type = "ldap" + + def format_attrs(self, attrs): + _attrs = {} + for k, v in attrs.items(): + if len(v) < 2: + v = v[0] + _attrs[k] = v + return _attrs + + def get_entry(self, key, filter_="", attrs=None, **kwargs): + filter_ = filter_ or "(objectClass=*)" + + entry = self.client.get(key, filter_=filter_, attributes=attrs) + if not entry: + return None + return Entry(entry.entry_dn, self.format_attrs(entry.entry_attributes_as_dict)) + + def modify_entry(self, key, attrs=None, **kwargs): + attrs = attrs or {} + del_flag = kwargs.get("delete_attr", False) + + if del_flag: + mod = self.client.MODIFY_DELETE + else: + mod = self.client.MODIFY_REPLACE + + for k, v in attrs.items(): + if not isinstance(v, list): + v = [v] + attrs[k] = [(mod, v)] + return self.client.modify(key, attrs) + + def search_entries(self, key, filter_="", attrs=None, **kwargs): + filter_ = filter_ or "(objectClass=*)" + entries = self.client.search(key, filter_, attrs) + + return [ + Entry(entry.entry_dn, self.format_attrs(entry.entry_attributes_as_dict)) + for entry in entries + ] + + +class SQLBackend: + def __init__(self, manager): + self.manager = manager + self.client = SqlClient(manager) + self.type = "sql" + + def get_entry(self, key, filter_="", attrs=None, **kwargs): + table_name = kwargs.get("table_name") + entry = self.client.get(table_name, key, attrs) + + if not entry: + return None + return Entry(key, entry) + + def modify_entry(self, key, attrs=None, **kwargs): + attrs = attrs or {} + table_name = kwargs.get("table_name") + return self.client.update(table_name, key, attrs), "" + + def search_entries(self, key, filter_="", attrs=None, **kwargs): + attrs = attrs or {} + table_name = kwargs.get("table_name") + return [ + Entry(entry["doc_id"], entry) + for entry in self.client.search(table_name, attrs) + ] + + +class CouchbaseBackend: + def __init__(self, manager): + self.manager = manager + self.client = CouchbaseClient(manager) + self.type = "couchbase" + + def get_entry(self, key, filter_="", attrs=None, **kwargs): + bucket = kwargs.get("bucket") + req = self.client.exec_query( + f"SELECT META().id, {bucket}.* FROM {bucket} USE KEYS '{key}'" + ) + if not req.ok: + return + + try: + _attrs = req.json()["results"][0] + id_ = _attrs.pop("id") + entry = Entry(id_, _attrs) + except IndexError: + entry = None + return entry + + def modify_entry(self, key, attrs=None, **kwargs): + bucket = kwargs.get("bucket") + del_flag = kwargs.get("delete_attr", False) + attrs = attrs or {} + + if del_flag: + kv = ",".join(attrs.keys()) + mod_kv = f"UNSET {kv}" + else: + kv = ",".join([ + "{}={}".format(k, json.dumps(v)) + for k, v in attrs.items() + ]) + mod_kv = f"SET {kv}" + + query = f"UPDATE {bucket} USE KEYS '{key}' {mod_kv}" + req = self.client.exec_query(query) + + if req.ok: + resp = req.json() + status = bool(resp["status"] == "success") + message = resp["status"] + else: + status = False + message = req.text or req.reason + return status, message + + def search_entries(self, key, filter_="", attrs=None, **kwargs): + bucket = kwargs.get("bucket") + req = self.client.exec_query( + f"SELECT META().id, {bucket}.* FROM {bucket} {filter_}" + ) + if not req.ok: + return [] + + entries = [] + for item in req.json()["results"]: + id_ = item.pop("id") + entries.append(Entry(id_, item)) + return entries + + +class SpannerBackend: + def __init__(self, manager): + self.manager = manager + self.client = SpannerClient(manager) + self.type = "spanner" + + def get_entry(self, key, filter_="", attrs=None, **kwargs): + table_name = kwargs.get("table_name") + entry = self.client.get(table_name, key, attrs) + + if not entry: + return None + return Entry(key, entry) + + def modify_entry(self, key, attrs=None, **kwargs): + attrs = attrs or {} + table_name = kwargs.get("table_name") + return self.client.update(table_name, key, attrs), "" + + def search_entries(self, key, filter_="", attrs=None, **kwargs): + attrs = attrs or {} + table_name = kwargs.get("table_name") + return [ + Entry(entry["doc_id"], entry) + for entry in self.client.search(table_name, attrs) + ] + + +BACKEND_CLASSES = { + "sql": SQLBackend, + "couchbase": CouchbaseBackend, + "spanner": SpannerBackend, + "ldap": LDAPBackend, +} + + +class Upgrade: + def __init__(self, manager): + self.manager = manager + + mapper = PersistenceMapper() + + backend_cls = BACKEND_CLASSES[mapper.mapping["default"]] + self.backend = backend_cls(manager) + + def invoke(self): + logger.info("Running upgrade process (if required)") + self.update_client_scopes() + + def get_all_scopes(self): + if self.backend.type in ("sql", "spanner"): + kwargs = {"table_name": "jansScope"} + entries = self.backend.search_entries(None, **kwargs) + elif self.backend.type == "couchbase": + kwargs = {"bucket": os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans")} + entries = self.backend.search_entries( + None, filter_="WHERE objectClass = 'jansScope'", **kwargs + ) + else: + # likely ldap + entries = self.backend.search_entries( + "ou=scopes,o=jans", filter_="(objectClass=jansScope)" + ) + + return { + entry.attrs["jansId"]: entry.attrs.get("dn") or entry.id + for entry in entries + } + + def update_client_scopes(self): + kwargs = {} + client_id = self.manager.config.get("scim_client_id") + id_ = f"inum={client_id},ou=clients,o=jans" + + if self.backend.type in ("sql", "spanner"): + kwargs = {"table_name": "jansClnt"} + id_ = doc_id_from_dn(id_) + elif self.backend.type == "couchbase": + kwargs = {"bucket": os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans")} + id_ = id_from_dn(id_) + + entry = self.backend.get_entry(id_, **kwargs) + + if not entry: + return + + if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + client_scopes = entry.attrs["jansScope"]["v"] + else: + client_scopes = entry.attrs.get("jansScope") or [] + + if not isinstance(client_scopes, list): + client_scopes = [client_scopes] + + # all scopes mapping from persistence + all_scopes = self.get_all_scopes() + + # all potential scopes for client + new_client_scopes = [] + + # extract config_api scopes within range of jansId defined in swagger + swagger = parse_swagger_file() + scim_jans_ids = list(swagger["components"]["securitySchemes"]["scim_oauth"]["flows"]["clientCredentials"]["scopes"].keys()) + scim_scopes = list({ + dn for jid, dn in all_scopes.items() + if jid in scim_jans_ids + }) + new_client_scopes += scim_scopes + + # find missing scopes from the client + diff = list(set(new_client_scopes).difference(client_scopes)) + + if diff: + if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + entry.attrs["jansScope"]["v"] = client_scopes + diff + else: + entry.attrs["jansScope"] = client_scopes + diff + self.backend.modify_entry(entry.id, entry.attrs, **kwargs) + + +def main(): + manager = get_manager() + upgrade = Upgrade(manager) + upgrade.invoke() + + +if __name__ == "__main__": + main() diff --git a/docker-jans-scim/scripts/utils.py b/docker-jans-scim/scripts/utils.py new file mode 100644 index 00000000000..ce1e302f9b5 --- /dev/null +++ b/docker-jans-scim/scripts/utils.py @@ -0,0 +1,8 @@ +import ruamel.yaml + + +def parse_swagger_file(path="/app/static/jans-scim-openapi.yaml"): + with open(path) as f: + txt = f.read() + txt = txt.replace("\t", " ") + return ruamel.yaml.load(txt, Loader=ruamel.yaml.RoundTripLoader) diff --git a/docker-jans-scim/templates/jans-scim/clients.ldif b/docker-jans-scim/templates/jans-scim/clients.ldif index 58ceb45abbd..f74bd9763e1 100644 --- a/docker-jans-scim/templates/jans-scim/clients.ldif +++ b/docker-jans-scim/templates/jans-scim/clients.ldif @@ -6,16 +6,7 @@ jansAppTyp: native jansAttrs: {} jansClntSecret: %(scim_client_encoded_pw)s jansGrantTyp: client_credentials -jansScope: inum=1200.2B7428,ou=scopes,o=jans -jansScope: inum=1200.0A0198,ou=scopes,o=jans -jansScope: inum=1200.E14714,ou=scopes,o=jans -jansScope: inum=1200.178DAF,ou=scopes,o=jans -jansScope: inum=1200.57E5E9,ou=scopes,o=jans -jansScope: inum=1200.D0F7EB,ou=scopes,o=jans -jansScope: inum=1200.99AD30,ou=scopes,o=jans -jansScope: inum=1200.D5527D,ou=scopes,o=jans -jansScope: inum=1200.83383D,ou=scopes,o=jans -jansScope: inum=1200.0D9EB4,ou=scopes,o=jans +# scopes will be added dynamically using script jansSubjectTyp: pairwise jansTknEndpointAuthMethod: client_secret_basic objectClass: top diff --git a/docker-jans-scim/templates/jans-scim/scopes.ldif b/docker-jans-scim/templates/jans-scim/scopes.ldif deleted file mode 100644 index cc890a09d33..00000000000 --- a/docker-jans-scim/templates/jans-scim/scopes.ldif +++ /dev/null @@ -1,101 +0,0 @@ -# dn: inum=1200.2B7428,ou=scopes,o=jans -# description: Query user resources -# displayName: SCIM https://jans.io/scim/users.read -# inum: 1200.2B7428 -# jansDefScope: false -# jansId: https://jans.io/scim/users.read -# jansScopeTyp: oauth -# objectClass: top -# objectClass: jansScope -# jansAttrs: {"spontaneousClientId":null,"spontaneousClientScopes":null,"showInConfigurationEndpoint":true} - -# dn: inum=1200.0A0198,ou=scopes,o=jans -# description: Modify user resources -# displayName: SCIM https://jans.io/scim/users.write -# inum: 1200.0A0198 -# jansDefScope: false -# jansId: https://jans.io/scim/users.write -# jansScopeTyp: oauth -# objectClass: top -# objectClass: jansScope -# jansAttrs: {"spontaneousClientId":null,"spontaneousClientScopes":null,"showInConfigurationEndpoint":true} - -dn: inum=1200.E14714,ou=scopes,o=jans -description: Query group resources -displayName: SCIM https://jans.io/scim/groups.read -inum: 1200.E14714 -jansDefScope: false -jansId: https://jans.io/scim/groups.read -jansScopeTyp: oauth -objectClass: top -objectClass: jansScope - -dn: inum=1200.178DAF,ou=scopes,o=jans -description: Modify group resources -displayName: SCIM https://jans.io/scim/groups.write -inum: 1200.178DAF -jansDefScope: false -jansId: https://jans.io/scim/groups.write -jansScopeTyp: oauth -objectClass: top -objectClass: jansScope - -dn: inum=1200.57E5E9,ou=scopes,o=jans -description: Query fido resources -displayName: SCIM https://jans.io/scim/fido.read -inum: 1200.57E5E9 -jansDefScope: false -jansId: https://jans.io/scim/fido.read -jansScopeTyp: oauth -objectClass: top -objectClass: jansScope - -dn: inum=1200.D0F7EB,ou=scopes,o=jans -description: Modify fido resources -displayName: SCIM https://jans.io/scim/fido.write -inum: 1200.D0F7EB -jansDefScope: false -jansId: https://jans.io/scim/fido.write -jansScopeTyp: oauth -objectClass: top -objectClass: jansScope - -dn: inum=1200.99AD30,ou=scopes,o=jans -description: Query fido 2 resources -displayName: SCIM https://jans.io/scim/fido2.read -inum: 1200.99AD30 -jansDefScope: false -jansId: https://jans.io/scim/fido2.read -jansScopeTyp: oauth -objectClass: top -objectClass: jansScope - -dn: inum=1200.D5527D,ou=scopes,o=jans -description: Modify fido 2 resources -displayName: SCIM https://jans.io/scim/fido2.write -inum: 1200.D5527D -jansDefScope: false -jansId: https://jans.io/scim/fido2.write -jansScopeTyp: oauth -objectClass: top -objectClass: jansScope - -dn: inum=1200.83383D,ou=scopes,o=jans -description: Access the root .search endpoint -displayName: SCIM https://jans.io/scim/all-resources.search -inum: 1200.83383D -jansDefScope: false -jansId: https://jans.io/scim/all-resources.search -jansScopeTyp: oauth -objectClass: top -objectClass: jansScope - -dn: inum=1200.0D9EB4,ou=scopes,o=jans -description: Send requests to the bulk endpoint -displayName: SCIM https://jans.io/scim/bulk -inum: 1200.0D9EB4 -jansDefScope: false -jansId: https://jans.io/scim/bulk -jansScopeTyp: oauth -objectClass: top -objectClass: jansScope diff --git a/jans-pycloudlib/jans/pycloudlib/wait.py b/jans-pycloudlib/jans/pycloudlib/wait.py index d07c92528bd..35ee1043ab5 100644 --- a/jans-pycloudlib/jans/pycloudlib/wait.py +++ b/jans-pycloudlib/jans/pycloudlib/wait.py @@ -183,9 +183,9 @@ def wait_for_ldap(manager: Manager, **kwargs: _t.Any) -> None: manager: An instance of manager class. **kwargs: Arbitrary keyword arguments (see Other Parameters section, if any). """ - jca_client_id = manager.config.get("jca_client_id") + client_id = manager.config.get("role_based_client_id") search_mapping = { - "default": (f"inum={jca_client_id},ou=clients,o=jans", "(objectClass=jansClnt)"), + "default": (f"inum={client_id},ou=clients,o=jans", "(objectClass=jansClnt)"), "user": (_ADMIN_GROUP_DN, "(objectClass=jansGrp)"), "site": ("ou=cache-refresh,o=site", "(ou=cache-refresh)"), "cache": ("ou=cache,o=jans", "(ou=cache)"), @@ -228,9 +228,9 @@ def wait_for_couchbase(manager: Manager, **kwargs: _t.Any) -> None: **kwargs: Arbitrary keyword arguments (see Other Parameters section, if any). """ bucket_prefix = os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans") - jca_client_id = manager.config.get("jca_client_id") + client_id = manager.config.get("role_based_client_id") search_mapping = { - "default": (id_from_dn(f"inum={jca_client_id},ou=clients,o=jans"), f"{bucket_prefix}"), + "default": (id_from_dn(f"inum={client_id},ou=clients,o=jans"), f"{bucket_prefix}"), "user": (id_from_dn(_ADMIN_GROUP_DN), f"{bucket_prefix}_user"), } @@ -284,9 +284,9 @@ def wait_for_sql(manager: Manager, **kwargs: _t.Any) -> None: manager: An instance of manager class. **kwargs: Arbitrary keyword arguments (see Other Parameters section, if any). """ - jca_client_id = manager.config.get("jca_client_id") + client_id = manager.config.get("role_based_client_id") search_mapping = { - "default": (doc_id_from_dn(f"inum={jca_client_id},ou=clients,o=jans"), "jansClnt"), + "default": (doc_id_from_dn(f"inum={client_id},ou=clients,o=jans"), "jansClnt"), "user": (doc_id_from_dn(_ADMIN_GROUP_DN), "jansGrp"), } @@ -325,9 +325,9 @@ def wait_for_spanner(manager: Manager, **kwargs: _t.Any) -> None: manager: An instance of manager class. **kwargs: Arbitrary keyword arguments (see Other Parameters section, if any). """ - jca_client_id = manager.config.get("jca_client_id") + client_id = manager.config.get("role_based_client_id") search_mapping = { - "default": (doc_id_from_dn(f"inum={jca_client_id},ou=clients,o=jans"), "jansClnt"), + "default": (doc_id_from_dn(f"inum={client_id},ou=clients,o=jans"), "jansClnt"), "user": (doc_id_from_dn(_ADMIN_GROUP_DN), "jansGrp"), }