def encode_template(fn, ctx, base_dir="/app/templates"):
    path = os.path.join(base_dir, fn)
    # ctx is nested which has `config` and `secret` keys
    data = {}
    for _, v in ctx.items():
        data.update(v)
    with open(path) as f:
        return generate_base64_contents(safe_render(f.read(), data))
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_scim_ctx(ctx):
    basedir = '/app/templates/jans-scim'
    file_mappings = {
        'scim_dynamic_conf_base64': 'dynamic-conf.json',
        'scim_static_conf_base64': 'static-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() % ctx)
    return ctx
def merge_auth_ctx(ctx):
    basedir = '/app/templates/jans-auth'
    file_mappings = {
        'auth_static_conf_base64': 'static-conf.json',
        'auth_error_base64': 'errors.json',
    }

    if os.environ.get("CN_DISTRIBUTION", "default") == "openbanking":
        file_mappings["auth_config_base64"] = "dynamic-conf.ob.json"
    else:
        file_mappings["auth_config_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() % ctx)
    return ctx
def merge_extension_ctx(ctx):
    basedir = "/app/static/extension"

    if os.environ.get("CN_DISTRIBUTION", "default") == "openbanking":
        basedir = "/app/static/ob_extension"

    for ext_type in os.listdir(basedir):
        ext_type_dir = os.path.join(basedir, ext_type)

        for fname in os.listdir(ext_type_dir):
            filepath = os.path.join(ext_type_dir, fname)
            ext_name = "{}_{}".format(ext_type,
                                      os.path.splitext(fname)[0].lower())

            with open(filepath) as fd:
                ctx[ext_name] = generate_base64_contents(fd.read())
    return ctx
    def prune(self):
        # avoid conflict with external JWKS URI
        ext_jwks_uri = os.environ.get("CN_OB_EXT_SIGNING_JWKS_URI", "")
        if ext_jwks_uri:
            logger.warning(f"Found external JWKS URI at {ext_jwks_uri}; "
                           "skipping proccess to avoid conflict with "
                           "builtin key rotation feature in jans-auth")
            return

        config = self.backend.get_auth_config()

        if not config:
            # search failed due to missing entry
            logger.warning("Unable to find jans-auth config")
            return

        try:
            conf_dynamic = json.loads(config["jansConfDyn"])
        except TypeError:  # not string/buffer
            conf_dynamic = config["jansConfDyn"]

        if conf_dynamic["keyRegenerationEnabled"]:
            logger.warning("keyRegenerationEnabled config was set to true; "
                           "skipping proccess to avoid conflict with "
                           "builtin key rotation feature in jans-auth")
            return

        jks_pass = self.manager.secret.get("auth_openid_jks_pass")

        conf_dynamic.update({
            "keyRegenerationEnabled":
            False,  # always set to False
            "webKeysStorage":
            "keystore",
            "keyStoreSecret":
            jks_pass,
            "keyAlgsAllowedForGeneration":
            self.allowed_key_algs,
            "keyStoreFile":
            self.manager.config.get("auth_openid_jks_fn"),
            "jwksUri":
            f"https://{self.manager.config.get('hostname')}/jans-auth/restv1/jwks",
        })

        # get old JWKS from persistence
        try:
            web_keys = json.loads(config["jansConfWebKeys"])
        except TypeError:
            web_keys = config["jansConfWebKeys"]

        logger.info("Cleaning up keys (if any)")

        jks_fn = "/etc/certs/auth-keys.jks"
        self.manager.secret.to_file("auth_jks_base64",
                                    jks_fn,
                                    decode=True,
                                    binary_mode=True)

        # non-pruned keys
        new_jwks = []

        # make sure keys sorted by newer ``exp`` first, so the older one
        # won't be added to new JWKS
        old_jwks = web_keys.get("keys", [])
        old_jwks = sorted(old_jwks, key=lambda k: k["exp"], reverse=True)

        cnt = Counter(j["alg"] for j in new_jwks)

        for jwk in old_jwks:
            # exclude alg if it's not allowed
            if jwk["alg"] not in self.allowed_key_algs:
                keytool_delete_key(jks_fn, jwk["kid"], jks_pass)
                continue

            # cannot have more than 1 key for same algorithm in new JWKS
            if cnt[jwk["alg"]]:
                keytool_delete_key(jks_fn, jwk["kid"], jks_pass)
                continue

            # preserve the key
            new_jwks.append(jwk)
            cnt[jwk["alg"]] += 1

        web_keys["keys"] = new_jwks

        jwks_fn = "/etc/certs/auth-keys.json"
        with open(jwks_fn, "w") as f:
            f.write(json.dumps(web_keys, indent=2))

        if self.dry_run:
            return

        auth_containers = []

        if self.push_keys:
            auth_containers = self.meta_client.get_containers(
                "APP_NAME=auth-server")
            if not auth_containers:
                logger.warning(
                    "Unable to find any jans-auth container; make sure "
                    "to deploy jans-auth and set APP_NAME=auth-server "
                    "label on container level")
                # exit immediately to avoid persistence/secrets being modified
                return

        for container in auth_containers:
            name = self.meta_client.get_container_name(container)

            logger.info(f"creating backup of {name}:{jks_fn}")
            self.meta_client.exec_cmd(container,
                                      f"cp {jks_fn} {jks_fn}.backup")
            logger.info(f"creating new {name}:{jks_fn}")
            self.meta_client.copy_to_container(container, jks_fn)

            logger.info(f"creating backup of {name}:{jwks_fn}")
            self.meta_client.exec_cmd(container,
                                      f"cp {jwks_fn} {jwks_fn}.backup")
            logger.info(f"creating new {name}:{jwks_fn}")
            self.meta_client.copy_to_container(container, jwks_fn)

        try:
            with open(jwks_fn) as f:
                keys = json.loads(f.read())

            logger.info("modifying jans-auth configuration")
            rev = int(config["jansRevision"])
            modified = self.backend.modify_auth_config(
                config["id"],
                rev + 1,
                conf_dynamic,
                keys,
            )

            if not modified:
                # restore jks and jwks
                logger.warning("failed to modify jans-auth configuration")
                for container in auth_containers:
                    name = self.meta_client.get_container_name(container)
                    logger.info(f"restoring backup of {name}:{jks_fn}")
                    self.meta_client.exec_cmd(container,
                                              f"cp {jks_fn}.backup {jks_fn}")
                    logger.info(f"restoring backup of {name}:{jwks_fn}")
                    self.meta_client.exec_cmd(
                        container, f"cp {jwks_fn}.backup {jwks_fn}")
                return

            self.manager.secret.set("auth_jks_base64",
                                    encode_jks(self.manager))
            self.manager.config.set("auth_key_rotated_at", int(time.time()))
            self.manager.secret.set("auth_openid_jks_pass", jks_pass)
            self.manager.config.set("auth_sig_keys", self.sig_keys)
            self.manager.config.set("auth_enc_keys", self.enc_keys)
            # jwks
            self.manager.secret.set(
                "auth_openid_key_base64",
                generate_base64_contents(json.dumps(keys)),
            )
        except (
                TypeError,
                ValueError,
        ) as exc:
            logger.warning(f"Unable to get public keys; reason={exc}")
    def patch(self):
        # avoid conflict with external JWKS URI
        ext_jwks_uri = os.environ.get("CN_OB_EXT_SIGNING_JWKS_URI", "")
        if ext_jwks_uri:
            logger.warning(f"Found external JWKS URI at {ext_jwks_uri}; "
                           "skipping proccess to avoid conflict with "
                           "builtin key rotation feature in jans-auth")
            return

        strategies = ", ".join(KEY_STRATEGIES)

        if self.key_strategy not in KEY_STRATEGIES:
            logger.error(f"Key strategy must be one of {strategies}")
            sys.exit(1)

        if self.privkey_push_strategy not in KEY_STRATEGIES:
            logger.error(
                f"Private key push strategy must be one of {strategies}")
            sys.exit(1)

        push_delay_invalid = False
        try:
            if int(self.privkey_push_delay) < 0:
                push_delay_invalid = True
        except ValueError:
            push_delay_invalid = True

        if push_delay_invalid:
            logger.error("Invalid integer value for private key push delay")
            sys.exit(1)

        config = self.backend.get_auth_config()

        if not config:
            # search failed due to missing entry
            logger.warning("Unable to find jans-auth config")
            return

        try:
            conf_dynamic = json.loads(config["jansConfDyn"])
        except TypeError:  # not string/buffer
            conf_dynamic = config["jansConfDyn"]

        if conf_dynamic["keyRegenerationEnabled"]:
            logger.warning("keyRegenerationEnabled config was set to true; "
                           "skipping proccess to avoid conflict with "
                           "builtin key rotation feature in jans-auth")
            return

        jks_pass = self.manager.secret.get("auth_openid_jks_pass")

        conf_dynamic.update({
            "keyRegenerationEnabled":
            False,  # always set to False
            "keyRegenerationInterval":
            int(self.rotation_interval),
            "webKeysStorage":
            "keystore",
            "keyStoreSecret":
            jks_pass,
            "keySelectionStrategy":
            self.key_strategy,
            "keyAlgsAllowedForGeneration":
            self.allowed_key_algs,
            "keyStoreFile":
            self.manager.config.get("auth_openid_jks_fn"),
            "jwksUri":
            f"https://{self.manager.config.get('hostname')}/jans-auth/restv1/jwks",
        })

        # get old JWKS from persistence
        try:
            web_keys = json.loads(config["jansConfWebKeys"])
        except TypeError:
            web_keys = config["jansConfWebKeys"]

        with open("/etc/certs/auth-keys.old.json", "w") as f:
            f.write(json.dumps(web_keys, indent=2))

        exp_hours = int(self.rotation_interval) + int(
            conf_dynamic["idTokenLifetime"] / 3600)

        jwks_fn, jks_fn = self.get_merged_keys(exp_hours)

        if self.dry_run:
            return

        auth_containers = []

        if self.push_keys:
            auth_containers = self.meta_client.get_containers(
                "APP_NAME=auth-server")
            if not auth_containers:
                logger.warning(
                    "Unable to find any jans-auth container; make sure "
                    "to deploy jans-auth and set APP_NAME=auth-server "
                    "label on container level")
                # exit immediately to avoid persistence/secrets being modified
                return

        for container in auth_containers:
            name = self.meta_client.get_container_name(container)

            logger.info(f"creating backup of {name}:{jwks_fn}")
            self.meta_client.exec_cmd(container,
                                      f"cp {jwks_fn} {jwks_fn}.backup")
            logger.info(f"creating new {name}:{jwks_fn}")
            self.meta_client.copy_to_container(container, jwks_fn)

            if int(self.privkey_push_delay) > 0:
                # delayed jks push
                continue

            logger.info(f"creating backup of {name}:{jks_fn}")
            self.meta_client.exec_cmd(container,
                                      f"cp {jks_fn} {jks_fn}.backup")
            logger.info(f"creating new {name}:{jks_fn}")
            self.meta_client.copy_to_container(container, jks_fn)

        try:
            with open(jwks_fn) as f:
                keys = json.loads(f.read())

            logger.info("modifying jans-auth configuration")
            logger.info(f"using keySelectionStrategy {self.key_strategy}")
            rev = int(config["jansRevision"]) + 1
            modified = self.backend.modify_auth_config(
                config["id"],
                rev,
                conf_dynamic,
                keys,
            )

            if not modified:
                # restore jks and jwks
                logger.warning("failed to modify jans-auth configuration")
                for container in auth_containers:
                    logger.info(f"restoring backup of {name}:{jwks_fn}")
                    self.meta_client.exec_cmd(
                        container, f"cp {jwks_fn}.backup {jwks_fn}")

                    if int(self.privkey_push_delay) > 0:
                        # delayed jks revert
                        continue

                    name = self.meta_client.get_container_name(container)
                    logger.info(f"restoring backup of {name}:{jks_fn}")
                    self.meta_client.exec_cmd(container,
                                              f"cp {jks_fn}.backup {jks_fn}")
                return

            self.manager.secret.set("auth_jks_base64",
                                    encode_jks(self.manager))
            self.manager.config.set("auth_key_rotated_at", int(time.time()))
            self.manager.secret.set("auth_openid_jks_pass", jks_pass)
            self.manager.config.set("auth_sig_keys", self.sig_keys)
            self.manager.config.set("auth_enc_keys", self.enc_keys)
            # jwks
            self.manager.secret.set(
                "auth_openid_key_base64",
                generate_base64_contents(json.dumps(keys)),
            )

            # publish delayed jks
            if int(self.privkey_push_delay) > 0:
                logger.info(
                    f"Waiting for private key push delay ({int(self.privkey_push_delay)} seconds) ..."
                )
                time.sleep(int(self.privkey_push_delay))
                for container in auth_containers:
                    logger.info(f"creating new {name}:{jks_fn}")
                    self.meta_client.copy_to_container(container, jks_fn)

                # key selection is changed
                if self.privkey_push_strategy != self.key_strategy:
                    rev = rev + 1
                    conf_dynamic.update({
                        "keySelectionStrategy":
                        self.privkey_push_strategy,
                    })

                    logger.info(
                        f"using keySelectionStrategy {self.privkey_push_strategy}"
                    )

                    self.backend.modify_auth_config(
                        config["id"],
                        rev,
                        conf_dynamic,
                        keys,
                    )
        except (
                TypeError,
                ValueError,
        ) as exc:
            logger.warning(f"Unable to get public keys; reason={exc}")
Beispiel #8
0
def test_generate_base64_contents(text, num_spaces, expected):
    from jans.pycloudlib.utils import generate_base64_contents
    assert generate_base64_contents(text, num_spaces) == expected