From 3ab6a1167328625c26b32d2b3b7cc46d37216261 Mon Sep 17 00:00:00 2001 From: Isman Firmansyah Date: Thu, 14 Apr 2022 03:10:18 +0700 Subject: [PATCH] feat: pre-populate role scope mapping dynamically (#1201) --- docker-jans-persistence-loader/Dockerfile | 8 +- .../requirements.txt | 1 + .../scripts/upgrade.py | 93 ++++++++++++++++++- .../scripts/utils.py | 42 +++++++++ .../jans-config-api/dynamic-conf.json | 10 ++ 5 files changed, 150 insertions(+), 4 deletions(-) diff --git a/docker-jans-persistence-loader/Dockerfile b/docker-jans-persistence-loader/Dockerfile index 53fb533067c..e983b723bb3 100644 --- a/docker-jans-persistence-loader/Dockerfile +++ b/docker-jans-persistence-loader/Dockerfile @@ -15,14 +15,14 @@ RUN apk update \ # ====== COPY requirements.txt /app/requirements.txt -RUN pip3 install -U pip wheel \ +RUN pip3 install --no-cache-dir -U pip wheel \ && pip3 install --no-cache-dir --default-timeout=300 -r /app/requirements.txt # ===================== # jans-linux-setup sync # ===================== -ENV JANS_LINUX_SETUP_VERSION=cfdc70e6a2ddcc9d2ba4e3cf07df19344d613944 +ENV JANS_LINUX_SETUP_VERSION=f241cdfd569daf054c844978ee868f1879442dbf ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) @@ -46,7 +46,6 @@ RUN cd /tmp/jans \ && cp ${JANS_SETUP_DIR}/schema/custom_schema.json /app/schema/custom_schema.json \ && cp ${JANS_SETUP_DIR}/static/opendj/index.json /app/static/opendj/index.json - RUN mkdir -p /app/templates/jans-config-api # partially sync templates from linux-setup @@ -62,6 +61,9 @@ RUN cd /tmp/jans \ && 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 +RUN wget -q https://github.com/JanssenProject/jans/raw/${JANS_LINUX_SETUP_VERSION}/jans-config-api/docs/jans-config-api-swagger.yaml -P /app/static + # TODO: casa should be moved from this image ENV GLUU_CASA_VERSION=a8251496ff2ade9dd8101873b45f4c490ae9c64e RUN wget -q https://github.com/GluuFederation/flex/raw/${GLUU_CASA_VERSION}/casa/extras/Casa.py -O /app/static/extension/person_authentication/Casa.py diff --git a/docker-jans-persistence-loader/requirements.txt b/docker-jans-persistence-loader/requirements.txt index 7a0bb2e3365..1b4a596bcaa 100644 --- a/docker-jans-persistence-loader/requirements.txt +++ b/docker-jans-persistence-loader/requirements.txt @@ -2,4 +2,5 @@ grpcio==1.41.0 ldif==4.1.1 libcst<0.4 +ruamel.yaml==0.16.10 git+https://github.com/JanssenProject/jans@abc89dc6fadae5627a68a97ab4f4f5ceb56af809#egg=jans-pycloudlib&subdirectory=jans-pycloudlib diff --git a/docker-jans-persistence-loader/scripts/upgrade.py b/docker-jans-persistence-loader/scripts/upgrade.py index c19025c9596..e3945ca426e 100644 --- a/docker-jans-persistence-loader/scripts/upgrade.py +++ b/docker-jans-persistence-loader/scripts/upgrade.py @@ -20,6 +20,7 @@ from settings import LOGGING_CONFIG from utils import doc_id_from_dn from utils import id_from_dn +from utils import get_role_scope_mappings logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("entrypoint") @@ -347,7 +348,6 @@ def __init__(self, manager): self.backend = backend_cls(manager) def invoke(self): - # TODO: refactor all self.backend.update_ to this class method logger.info("Running upgrade process (if required)") self.update_people_entries() @@ -362,6 +362,8 @@ def invoke(self): self.update_auth_dynamic_config() 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 @@ -647,3 +649,92 @@ def _update_token_server_client(): _update_jca_client() _update_token_server_client() + + def update_admin_ui_config(self): + kwargs = {} + id_ = "ou=admin-ui,ou=configuration,o=jans" + + if self.backend.type in ("sql", "spanner"): + kwargs = {"table_name": "jansAdminConfDyn"} + 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 + + # calculate new permissions for api-admin + role_mapping = get_role_scope_mappings() + api_admin_perms = [] + + for api_role in role_mapping["rolePermissionMapping"]: + if api_role["role"] == "api-admin": + api_admin_perms = api_role["permissions"] + break + + # current permissions + current_role_mapping = json.loads(entry.attrs["jansConfDyn"]) + should_update = False + + for i, api_role in enumerate(current_role_mapping["rolePermissionMapping"]): + if api_role["role"] == "api-admin": + # compare permissions between the ones from persistence (current) and newer permissions + if sorted(api_role["permissions"]) != sorted(api_admin_perms): + current_role_mapping["rolePermissionMapping"][i]["permissions"] = api_admin_perms + should_update = True + break + + if should_update: + entry.attrs["jansConfDyn"] = json.dumps(current_role_mapping) + 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 _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 + return conf, should_update diff --git a/docker-jans-persistence-loader/scripts/utils.py b/docker-jans-persistence-loader/scripts/utils.py index 6c0e2bf5c9a..94c7b131772 100644 --- a/docker-jans-persistence-loader/scripts/utils.py +++ b/docker-jans-persistence-loader/scripts/utils.py @@ -2,10 +2,12 @@ import base64 import json import os +from itertools import chain from pathlib import Path from urllib.parse import urlparse from uuid import uuid4 +import ruamel.yaml from ldap3.utils import dn as dnutils from jans.pycloudlib.utils import as_boolean @@ -231,6 +233,9 @@ def merge_auth_ctx(ctx): file_path = os.path.join(basedir, file_) with open(file_path) as fp: ctx[key] = generate_base64_contents(fp.read() % ctx) + + # determine role scope mappings + ctx["role_scope_mappings"] = json.dumps(get_role_scope_mappings()) return ctx @@ -486,3 +491,40 @@ def id_from_dn(dn): # the actual key return '_'.join(dns) or "_" + + +def get_config_api_swagger(path="/app/static/jans-config-api-swagger.yaml"): + with open(path) as f: + txt = f.read() + txt = txt.replace("\t", " ") + return ruamel.yaml.load(txt, Loader=ruamel.yaml.RoundTripLoader) + + +def get_config_api_scopes(): + swagger = get_config_api_swagger() + scope_list = [] + + for _, methods in swagger["paths"].items(): + for _, attrs in methods.items(): + if "security" not in attrs: + continue + scope_list += [attr["oauth2"] for attr in attrs["security"]] + + # make sure there's no duplication + return list(set(chain(*scope_list))) + + +def get_role_scope_mappings(path="/app/templates/jans-auth/role-scope-mappings.json"): + with open(path) as f: + role_mapping = json.loads(f.read()) + + scope_list = get_config_api_scopes() + + for i, api_role in enumerate(role_mapping["rolePermissionMapping"]): + if api_role["role"] == "api-admin": + # merge scopes without duplication + role_mapping["rolePermissionMapping"][i]["permissions"] = list(set( + role_mapping["rolePermissionMapping"][i]["permissions"] + scope_list + )) + break + return role_mapping 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 index 379bea65daf..2381ba31cad 100644 --- a/docker-jans-persistence-loader/templates/jans-config-api/dynamic-conf.json +++ b/docker-jans-persistence-loader/templates/jans-config-api/dynamic-conf.json @@ -33,5 +33,15 @@ "corsRequestDecorate": true, "corsEnabled": true } + ], + "userExclusionAttributes": [ + "userPassword" + ], + "userMandatoryAttributes": [ + "mail", + "displayName", + "jansStatus", + "userPassword", + "givenName" ] }