From 5388bb9c51c5e554180c1b1cfe1a8cae52cb8f7a Mon Sep 17 00:00:00 2001 From: Dixit Sathwara Date: Fri, 3 Apr 2026 17:14:55 +0530 Subject: [PATCH 1/9] [patch] add support permission mode during suite install --- src/mas/devops/templates/pipelinerun-install.yml.j2 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/mas/devops/templates/pipelinerun-install.yml.j2 b/src/mas/devops/templates/pipelinerun-install.yml.j2 index eebbbd5a..9e0b7471 100644 --- a/src/mas/devops/templates/pipelinerun-install.yml.j2 +++ b/src/mas/devops/templates/pipelinerun-install.yml.j2 @@ -553,6 +553,10 @@ spec: - name: mas_feature_usage value: "{{ mas_feature_usage }}" {%- endif %} +{%- if mas_permission_mode is defined and mas_permission_mode != "" %} + - name: mas_permission_mode + value: "{{ mas_permission_mode }}" +{%- endif %} # MAS Workspace # ------------------------------------------------------------------------- From fb903e85431d00b16e3f76c464d133ce57aba695 Mon Sep 17 00:00:00 2001 From: Dixit Sathwara Date: Sat, 25 Apr 2026 07:18:38 +0530 Subject: [PATCH 2/9] [patch] add selected apps var --- src/mas/devops/templates/pipelinerun-install.yml.j2 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/mas/devops/templates/pipelinerun-install.yml.j2 b/src/mas/devops/templates/pipelinerun-install.yml.j2 index 9e0b7471..bf5fb033 100644 --- a/src/mas/devops/templates/pipelinerun-install.yml.j2 +++ b/src/mas/devops/templates/pipelinerun-install.yml.j2 @@ -557,6 +557,10 @@ spec: - name: mas_permission_mode value: "{{ mas_permission_mode }}" {%- endif %} +{%- if mas_selected_apps is defined and mas_selected_apps != "" %} + - name: mas_selected_apps + value: "{{ mas_selected_apps }}" +{%- endif %} # MAS Workspace # ------------------------------------------------------------------------- From 3cf0b9cb73007adeb162c296cb7e4ddd54cd0872 Mon Sep 17 00:00:00 2001 From: Dixit Sathwara Date: Sat, 2 May 2026 15:58:56 +0530 Subject: [PATCH 3/9] [patch] add the pre_install rbac apply function --- src/mas/devops/preinstall_rbac.py | 361 ++++++++++++++++++ .../templates/pipelinerun-install.yml.j2 | 4 - 2 files changed, 361 insertions(+), 4 deletions(-) create mode 100644 src/mas/devops/preinstall_rbac.py diff --git a/src/mas/devops/preinstall_rbac.py b/src/mas/devops/preinstall_rbac.py new file mode 100644 index 00000000..b95e2e9c --- /dev/null +++ b/src/mas/devops/preinstall_rbac.py @@ -0,0 +1,361 @@ +# ***************************************************************************** +# Copyright (c) 2024 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +import logging +import yaml + +from os import path, listdir + +from kubernetes import client as k8s_client +from openshift.dynamic import DynamicClient +from jinja2 import Environment + +logger = logging.getLogger(__name__) + +DEFAULT_PREINSTALL_MAS_RBAC_ROOT = "/opt/app-root/rbac" + + +def _validate_preinstall_mas_rbac_permission_mode(permissionMode: str) -> None: + if permissionMode not in {"cluster", "namespaced", "minimal"}: + raise ValueError(f"Unsupported permission mode: {permissionMode}") + + +def _validate_selected_apps(selectedApps: list[str] | None) -> set[str]: + if not selectedApps: + return set() + + validApps = { + "core", + "aiservice", + "arcgis", + "facilities", + "iot", + "manage", + "monitor", + "optimizer", + "predict", + "visualinspection" + } + + validatedApps = set() + for app in selectedApps: + if app not in validApps: + raise ValueError(f"Unsupported selected app: {app}") + validatedApps.add(app) + + return validatedApps + + +def _get_selected_operator_dirs(selectedApps: set[str]) -> set[str]: + + appToOperatorDir = { + "core": "ibm-mas", + "aiservice": "ibm-aiservice", + "arcgis": "ibm-mas-arcgis", + "facilities": "ibm-mas-facilities", + "iot": "ibm-mas-iot", + "manage": "ibm-mas-manage", + "monitor": "ibm-mas-monitor", + "optimizer": "ibm-mas-optimizer", + "predict": "ibm-mas-predict", + "visualinspection": "ibm-mas-visualinspection" + } + + return {appToOperatorDir[app] for app in selectedApps} + + +def _should_apply_preinstall_mas_rbac_file(fileName: str, permissionMode: str) -> bool: + lowerName = path.basename(fileName).lower() + + if lowerName == "kustomization.yaml": + return False + + if not (lowerName.endswith(".yml") or lowerName.endswith(".yaml")): + return False + + if permissionMode == "cluster": + return lowerName.startswith("cluster-role-") + + if permissionMode == "namespaced": + return lowerName.startswith("role-non-essential-") + + return False + + +def _collect_preinstall_mas_rbac_files_from_source( + sourceOperatorsRoot: str, + masVersion: str, + permissionMode: str, + operatorNames: set[str] | None = None +) -> list[str]: + if not path.isdir(sourceOperatorsRoot): + logger.debug(f"Skipping missing RBAC source root {sourceOperatorsRoot}") + return [] + + if operatorNames is None: + operatorNames = { + operatorName for operatorName in listdir(sourceOperatorsRoot) + if path.isdir(path.join(sourceOperatorsRoot, operatorName)) + } + + manifestFiles = [] + for operatorName in sorted(operatorNames): + operatorRoot = path.join(sourceOperatorsRoot, operatorName) + if not path.isdir(operatorRoot): + logger.debug(f"Skipping missing operator root {operatorRoot}") + continue + + versionDir = path.join(operatorRoot, "rbac", masVersion) + if not path.isdir(versionDir): + logger.debug(f"Skipping missing RBAC version directory {versionDir}") + continue + + for manifestName in sorted(listdir(versionDir)): + manifestFile = path.join(versionDir, manifestName) + if not path.isfile(manifestFile): + continue + + if _should_apply_preinstall_mas_rbac_file(manifestName, permissionMode): + manifestFiles.append(manifestFile) + + return manifestFiles + + +def _discover_preinstall_mas_rbac_files( + rbacRootDir: str | None, + masVersion: str, + permissionMode: str, + selectedApps: set[str] +) -> list[str]: + if not rbacRootDir: + rbacRootDir = DEFAULT_PREINSTALL_MAS_RBAC_ROOT + + if permissionMode == "minimal": + return [] + + selectedOperatorDirs = _get_selected_operator_dirs(selectedApps) + + sourceRoots = [ + ( + path.join(rbacRootDir, "maximo-operator-catalog", "operators"), + selectedOperatorDirs + ), + ( + path.join(rbacRootDir, "openshift-platform", "operators"), + None + ) + ] + + manifestFiles = [] + for sourceRoot, operatorNames in sourceRoots: + manifestFiles.extend( + _collect_preinstall_mas_rbac_files_from_source( + sourceOperatorsRoot=sourceRoot, + masVersion=masVersion, + permissionMode=permissionMode, + operatorNames=operatorNames + ) + ) + + return sorted(set(manifestFiles)) + + +def _get_preinstall_mas_rbac_namespaces(masInstanceId: str, permissionMode: str, selectedApps: set[str]) -> set[str]: + if permissionMode == "minimal": + return set() + + namespaces = {f"mas-{masInstanceId}-core"} + + if permissionMode == "cluster": + return namespaces + + appNamespaces = { + "aiservice": f"mas-{masInstanceId}-aiservice", + "arcgis": f"mas-{masInstanceId}-arcgis", + "facilities": f"mas-{masInstanceId}-facilities", + "iot": f"mas-{masInstanceId}-iot", + "manage": f"mas-{masInstanceId}-manage", + "monitor": f"mas-{masInstanceId}-monitor", + "optimizer": f"mas-{masInstanceId}-optimizer", + "predict": f"mas-{masInstanceId}-predict", + "visualinspection": f"mas-{masInstanceId}-visualinspection" + } + + for app in selectedApps: + if app in appNamespaces: + namespaces.add(appNamespaces[app]) + + return namespaces + + +def _check_self_subject_access( + dynClient: DynamicClient, + verb: str, + resource: str, + group: str = "rbac.authorization.k8s.io", + namespace: str | None = None +) -> bool: + authAPI = k8s_client.AuthorizationV1Api(dynClient.client) + review = k8s_client.V1SelfSubjectAccessReview( + spec=k8s_client.V1SelfSubjectAccessReviewSpec( + resource_attributes=k8s_client.V1ResourceAttributes( + namespace=namespace, + verb=verb, + resource=resource, + group=group + ) + ) + ) + result = authAPI.create_self_subject_access_review(body=review) + status = getattr(result, "status", None) + return bool(getattr(status, "allowed", False)) + + +def buildClusterAdminPermissionMatrix() -> list[dict[str, str]]: + return [ + {"verb": "create", "resource": "namespaces", "group": ""}, + {"verb": "create", "resource": "clusterroles"}, + {"verb": "update", "resource": "clusterroles"}, + {"verb": "create", "resource": "clusterrolebindings"}, + {"verb": "update", "resource": "clusterrolebindings"}, + ] + + +def permissionCheckForRBAC( + dynClient: DynamicClient, + checks: list[dict[str, str]] | None = None +) -> list[dict[str, str | bool]]: + if checks is None: + checks = buildClusterAdminPermissionMatrix() + + results = [] + + for check in checks: + verb = check["verb"] + resource = check["resource"] + group = check.get("group", "rbac.authorization.k8s.io") + namespace = check.get("namespace") + + allowed = _check_self_subject_access( + dynClient=dynClient, + verb=verb, + resource=resource, + group=group, + namespace=namespace + ) + + result: dict[str, str | bool] = { + "verb": verb, + "resource": resource, + "group": group, + "allowed": allowed + } + + if namespace is not None: + result["namespace"] = namespace + + results.append(result) + + return results + + +def applyPreInstallMASRBAC( + dynClient: DynamicClient, + masVersion: str, + masInstanceId: str, + permissionMode: str, + selectedApps: list[str] | None = None, + rbacRootDir: str | None = None +) -> None: + if not rbacRootDir: + rbacRootDir = DEFAULT_PREINSTALL_MAS_RBAC_ROOT + + validatedApps = _validate_selected_apps(selectedApps) + + if not validatedApps: + logger.info("No selected apps provided for pre-install MAS RBAC apply") + return + + manifestFiles = _discover_preinstall_mas_rbac_files( + rbacRootDir=rbacRootDir, + masVersion=masVersion, + permissionMode=permissionMode, + selectedApps=validatedApps + ) + + logger.info( + f"Applying pre-install MAS RBAC from {rbacRootDir} for MAS {masVersion}, " + f"masInstanceId={masInstanceId}, permissionMode={permissionMode}, " + f"selectedApps={sorted(validatedApps)}, " + f"manifestCount={len(manifestFiles)}" + ) + + if not manifestFiles: + logger.info("No pre-install MAS RBAC manifests selected for apply") + return + + namespaceAPI = dynClient.resources.get(api_version="v1", kind="Namespace") + requiredNamespaces = _get_preinstall_mas_rbac_namespaces( + masInstanceId=masInstanceId, + permissionMode=permissionMode, + selectedApps=validatedApps + ) + + for namespace in sorted(requiredNamespaces): + logger.info(f"Ensuring namespace exists for pre-install MAS RBAC: {namespace}") + namespaceAPI.apply(body={ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "name": namespace + } + }) + + env = Environment() + appliedResourceCount = 0 + + for manifestFile in manifestFiles: + logger.info(f"Applying pre-install MAS RBAC manifest {manifestFile}") + with open(manifestFile, "r") as file: + template = env.from_string(file.read()) + renderedManifest = template.render(mas_instance_id=masInstanceId) + + for resourceBody in yaml.safe_load_all(renderedManifest): + if resourceBody is None: + continue + + apiVersion = resourceBody["apiVersion"] + kind = resourceBody["kind"] + metadata = resourceBody["metadata"] + resourceName = metadata["name"] + resourceNamespace = metadata.get("namespace") + + if kind in {"Role", "RoleBinding"} and not resourceNamespace: + raise ValueError( + f"Namespaced RBAC resource {kind}/{resourceName} from {manifestFile} is missing metadata.namespace" + ) + + logger.debug( + f"Applying {kind} {resourceName} " + f"(apiVersion={apiVersion}, namespace={resourceNamespace})" + ) + + resourceAPI = dynClient.resources.get(api_version=apiVersion, kind=kind) + if resourceNamespace: + resourceAPI.apply(body=resourceBody, namespace=resourceNamespace) + else: + resourceAPI.apply(body=resourceBody) + + appliedResourceCount += 1 + + logger.info( + f"Pre-install MAS RBAC apply completed: processedFiles={len(manifestFiles)}, " + f"appliedResources={appliedResourceCount}" + ) diff --git a/src/mas/devops/templates/pipelinerun-install.yml.j2 b/src/mas/devops/templates/pipelinerun-install.yml.j2 index 88f70854..b86e8f41 100644 --- a/src/mas/devops/templates/pipelinerun-install.yml.j2 +++ b/src/mas/devops/templates/pipelinerun-install.yml.j2 @@ -561,10 +561,6 @@ spec: - name: mas_permission_mode value: "{{ mas_permission_mode }}" {%- endif %} -{%- if mas_selected_apps is defined and mas_selected_apps != "" %} - - name: mas_selected_apps - value: "{{ mas_selected_apps }}" -{%- endif %} # MAS Workspace # ------------------------------------------------------------------------- From 32f139bbf4f6637498209a6974836c19e8f3a53b Mon Sep 17 00:00:00 2001 From: Dixit Sathwara Date: Sat, 2 May 2026 20:16:51 +0530 Subject: [PATCH 4/9] [patch] add the new var --- src/mas/devops/preinstall_rbac.py | 12 ++++++------ src/mas/devops/templates/pipelinerun-install.yml.j2 | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/mas/devops/preinstall_rbac.py b/src/mas/devops/preinstall_rbac.py index b95e2e9c..1a11a81a 100644 --- a/src/mas/devops/preinstall_rbac.py +++ b/src/mas/devops/preinstall_rbac.py @@ -22,11 +22,6 @@ DEFAULT_PREINSTALL_MAS_RBAC_ROOT = "/opt/app-root/rbac" -def _validate_preinstall_mas_rbac_permission_mode(permissionMode: str) -> None: - if permissionMode not in {"cluster", "namespaced", "minimal"}: - raise ValueError(f"Unsupported permission mode: {permissionMode}") - - def _validate_selected_apps(selectedApps: list[str] | None) -> set[str]: if not selectedApps: return set() @@ -80,6 +75,11 @@ def _should_apply_preinstall_mas_rbac_file(fileName: str, permissionMode: str) - if not (lowerName.endswith(".yml") or lowerName.endswith(".yaml")): return False + # TODO: Sort out this openshift-ingress exception properly. + # For now, always apply this manifest in any permission mode. + if lowerName == "role-essential-core-entitymgr-suite-openshift-ingress.yaml": + return True + if permissionMode == "cluster": return lowerName.startswith("cluster-role-") @@ -164,7 +164,7 @@ def _discover_preinstall_mas_rbac_files( ) ) - return sorted(set(manifestFiles)) + return list(dict.fromkeys(manifestFiles)) def _get_preinstall_mas_rbac_namespaces(masInstanceId: str, permissionMode: str, selectedApps: set[str]) -> set[str]: diff --git a/src/mas/devops/templates/pipelinerun-install.yml.j2 b/src/mas/devops/templates/pipelinerun-install.yml.j2 index b86e8f41..b3ccea78 100644 --- a/src/mas/devops/templates/pipelinerun-install.yml.j2 +++ b/src/mas/devops/templates/pipelinerun-install.yml.j2 @@ -561,6 +561,10 @@ spec: - name: mas_permission_mode value: "{{ mas_permission_mode }}" {%- endif %} +{%- if mas_internal_certificate_issuer_kind is defined and mas_internal_certificate_issuer_kind != "" %} + - name: mas_internal_certificate_issuer_kind + value: "{{ mas_internal_certificate_issuer_kind }}" +{%- endif %} # MAS Workspace # ------------------------------------------------------------------------- From fe92762e041b013f46c031accb483b96fe7e2e1d Mon Sep 17 00:00:00 2001 From: Dixit Sathwara Date: Sun, 3 May 2026 16:59:58 +0530 Subject: [PATCH 5/9] rename file --- src/mas/devops/{preinstall_rbac.py => pre_install.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/mas/devops/{preinstall_rbac.py => pre_install.py} (100%) diff --git a/src/mas/devops/preinstall_rbac.py b/src/mas/devops/pre_install.py similarity index 100% rename from src/mas/devops/preinstall_rbac.py rename to src/mas/devops/pre_install.py From 439817bc85b6a09f636b0f76f23b0adc86694b71 Mon Sep 17 00:00:00 2001 From: Dixit Sathwara Date: Mon, 4 May 2026 09:18:06 +0530 Subject: [PATCH 6/9] enable preinstall for minimal mode --- src/mas/devops/pre_install.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/mas/devops/pre_install.py b/src/mas/devops/pre_install.py index 1a11a81a..6289eb76 100644 --- a/src/mas/devops/pre_install.py +++ b/src/mas/devops/pre_install.py @@ -136,9 +136,10 @@ def _discover_preinstall_mas_rbac_files( ) -> list[str]: if not rbacRootDir: rbacRootDir = DEFAULT_PREINSTALL_MAS_RBAC_ROOT - - if permissionMode == "minimal": - return [] + + # Due to ingresscontroller role we need to apply the preinstall RBAC for the minimal permission mode + # if permissionMode == "minimal": + # return [] selectedOperatorDirs = _get_selected_operator_dirs(selectedApps) @@ -168,8 +169,10 @@ def _discover_preinstall_mas_rbac_files( def _get_preinstall_mas_rbac_namespaces(masInstanceId: str, permissionMode: str, selectedApps: set[str]) -> set[str]: - if permissionMode == "minimal": - return set() + + # Due to ingresscontroller role we need to apply the preinstall RBAC for the minimal permission mode + # if permissionMode == "minimal": + # return set() namespaces = {f"mas-{masInstanceId}-core"} From f646de47ed85678f1fab877075ec29937123dd75 Mon Sep 17 00:00:00 2001 From: Dixit Sathwara Date: Mon, 4 May 2026 15:26:13 +0530 Subject: [PATCH 7/9] fix the pre-commit --- src/mas/devops/pre_install.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mas/devops/pre_install.py b/src/mas/devops/pre_install.py index 6289eb76..b8fbf0e1 100644 --- a/src/mas/devops/pre_install.py +++ b/src/mas/devops/pre_install.py @@ -136,7 +136,7 @@ def _discover_preinstall_mas_rbac_files( ) -> list[str]: if not rbacRootDir: rbacRootDir = DEFAULT_PREINSTALL_MAS_RBAC_ROOT - + # Due to ingresscontroller role we need to apply the preinstall RBAC for the minimal permission mode # if permissionMode == "minimal": # return [] @@ -169,7 +169,7 @@ def _discover_preinstall_mas_rbac_files( def _get_preinstall_mas_rbac_namespaces(masInstanceId: str, permissionMode: str, selectedApps: set[str]) -> set[str]: - + # Due to ingresscontroller role we need to apply the preinstall RBAC for the minimal permission mode # if permissionMode == "minimal": # return set() From e2c52a340e357bb1df384de93338b12daa306c91 Mon Sep 17 00:00:00 2001 From: Dixit Sathwara Date: Mon, 4 May 2026 15:45:44 +0530 Subject: [PATCH 8/9] remove namespace creation in cluster mode --- src/mas/devops/pre_install.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mas/devops/pre_install.py b/src/mas/devops/pre_install.py index b8fbf0e1..f0b65c16 100644 --- a/src/mas/devops/pre_install.py +++ b/src/mas/devops/pre_install.py @@ -174,10 +174,10 @@ def _get_preinstall_mas_rbac_namespaces(masInstanceId: str, permissionMode: str, # if permissionMode == "minimal": # return set() - namespaces = {f"mas-{masInstanceId}-core"} - if permissionMode == "cluster": - return namespaces + return set() + + namespaces = {f"mas-{masInstanceId}-core"} appNamespaces = { "aiservice": f"mas-{masInstanceId}-aiservice", From 07c8d9b7c41722677ce04c6572e8c2df218ff57b Mon Sep 17 00:00:00 2001 From: Dixit Sathwara Date: Tue, 5 May 2026 17:14:34 +0530 Subject: [PATCH 9/9] remove the permission mode from spec and update the issuerKind --- src/mas/devops/templates/pipelinerun-install.yml.j2 | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/mas/devops/templates/pipelinerun-install.yml.j2 b/src/mas/devops/templates/pipelinerun-install.yml.j2 index b3ccea78..e5890554 100644 --- a/src/mas/devops/templates/pipelinerun-install.yml.j2 +++ b/src/mas/devops/templates/pipelinerun-install.yml.j2 @@ -557,13 +557,9 @@ spec: - name: mas_feature_usage value: "{{ mas_feature_usage }}" {%- endif %} -{%- if mas_permission_mode is defined and mas_permission_mode != "" %} - - name: mas_permission_mode - value: "{{ mas_permission_mode }}" -{%- endif %} -{%- if mas_internal_certificate_issuer_kind is defined and mas_internal_certificate_issuer_kind != "" %} - - name: mas_internal_certificate_issuer_kind - value: "{{ mas_internal_certificate_issuer_kind }}" +{%- if mas_issuer_kind is defined and mas_issuer_kind != "" %} + - name: mas_issuer_kind + value: "{{ mas_issuer_kind }}" {%- endif %} # MAS Workspace