From 3853f155a631c7f7852ed018385cae55d8f9b5d5 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Mon, 21 Sep 2026 13:53:54 +0530 Subject: [PATCH 01/17] feat(rbac): add createPipeline/deletePipeline actions Co-Authored-By: Claude Opus 4.8 --- pkg/auth/authorisation/casbin/rbacpolicy.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/auth/authorisation/casbin/rbacpolicy.go b/pkg/auth/authorisation/casbin/rbacpolicy.go index 1bca4a6418..ab1d7c5066 100644 --- a/pkg/auth/authorisation/casbin/rbacpolicy.go +++ b/pkg/auth/authorisation/casbin/rbacpolicy.go @@ -64,6 +64,11 @@ const ( // admin/manager/super-admin keep app lifecycle via their wildcard (`*`) action policies. ActionCreateApp = "createApp" ActionDeleteApp = "deleteApp" + // ActionCreatePipeline and ActionDeletePipeline gate CI/CD pipeline and workflow lifecycle + // (create/delete), decoupled from ActionCreate/ActionUpdate/ActionDelete which continue to gate + // config editing. admin/manager/super-admin keep lifecycle via their wildcard (`*`) action policies. + ActionCreatePipeline = "createPipeline" + ActionDeletePipeline = "deletePipeline" ClusterResourceRegex = "%s/%s" // {cluster}/{namespace} ClusterObjectRegex = "%s/%s/%s" // {groupName}/{kindName}/{objectName} From f54dc8e0150cf3084549519c190a6c73d0959786 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Mon, 21 Sep 2026 13:54:10 +0530 Subject: [PATCH 02/17] feat(rbac): gate CI pipeline create on createPipeline Co-Authored-By: Claude Opus 4.8 --- .../app/pipeline/configure/BuildPipelineRestHandler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/restHandler/app/pipeline/configure/BuildPipelineRestHandler.go b/api/restHandler/app/pipeline/configure/BuildPipelineRestHandler.go index 8cdc04422d..6cfe2f1d85 100644 --- a/api/restHandler/app/pipeline/configure/BuildPipelineRestHandler.go +++ b/api/restHandler/app/pipeline/configure/BuildPipelineRestHandler.go @@ -141,7 +141,7 @@ func (handler *PipelineConfigRestHandlerImpl) CreateCiConfig(w http.ResponseWrit } token := r.Header.Get("token") - _, authorized := handler.getAppAndCheckAuthForAction(w, createRequest.AppId, token, casbin.ActionCreate) + _, authorized := handler.getAppAndCheckAuthForAction(w, createRequest.AppId, token, casbin.ActionCreatePipeline) if !authorized { return } From d9cb23868987ba8c517540a33818c7b36de62722 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Mon, 21 Sep 2026 13:54:26 +0530 Subject: [PATCH 03/17] feat(rbac): gate CD pipeline create on createPipeline Co-Authored-By: Claude Opus 4.8 --- .../app/pipeline/configure/DeploymentPipelineRestHandler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/restHandler/app/pipeline/configure/DeploymentPipelineRestHandler.go b/api/restHandler/app/pipeline/configure/DeploymentPipelineRestHandler.go index f0e82c9591..214a8b4281 100644 --- a/api/restHandler/app/pipeline/configure/DeploymentPipelineRestHandler.go +++ b/api/restHandler/app/pipeline/configure/DeploymentPipelineRestHandler.go @@ -233,7 +233,7 @@ func (handler *PipelineConfigRestHandlerImpl) CreateCdPipeline(w http.ResponseWr // RBAC resourceName := handler.enforcerUtil.GetAppRBACName(app.AppName) - if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionCreate, resourceName); !ok { + if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionCreatePipeline, resourceName); !ok { common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden) return } From 9fd821797d2da90e14f50c34d9c67e6ee80ad832 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Mon, 21 Sep 2026 13:54:49 +0530 Subject: [PATCH 04/17] fix(rbac): gate CD_CREATE via PatchCdPipeline on createPipeline Co-Authored-By: Claude Opus 4.8 --- .../configure/DeploymentPipelineRestHandler.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/api/restHandler/app/pipeline/configure/DeploymentPipelineRestHandler.go b/api/restHandler/app/pipeline/configure/DeploymentPipelineRestHandler.go index 214a8b4281..d1e149d894 100644 --- a/api/restHandler/app/pipeline/configure/DeploymentPipelineRestHandler.go +++ b/api/restHandler/app/pipeline/configure/DeploymentPipelineRestHandler.go @@ -367,13 +367,23 @@ func (handler *PipelineConfigRestHandlerImpl) PatchCdPipeline(w http.ResponseWri common.WriteJsonResp(w, err, nil, http.StatusBadRequest) return } + // CD_CREATE adds a pipeline (lifecycle create); CD_DELETE/CD_DELETE_PARTIAL delete one (lifecycle + // delete); any other patch (CD_UPDATE / config / strategy) stays on update so config editing works. + appAction := casbin.ActionUpdate + switch cdPipeline.Action { + case bean.CD_CREATE: + appAction = casbin.ActionCreatePipeline + case bean.CD_DELETE, bean.CD_DELETE_PARTIAL: + appAction = casbin.ActionDeletePipeline + } resourceName := handler.enforcerUtil.GetAppRBACName(app.AppName) - if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionUpdate, resourceName); !ok { + if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, appAction, resourceName); !ok { common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden) return } object := handler.enforcerUtil.GetAppRBACByAppIdAndPipelineId(cdPipeline.AppId, cdPipeline.Pipeline.Id) + // keep environment:update for the env-scoped gate on both delete and config patches if ok := handler.enforcer.Enforce(token, casbin.ResourceEnvironment, casbin.ActionUpdate, object); !ok { common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden) return From 1dcfb9726e1cff470f57329e6cad029b9f9c6a12 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Mon, 21 Sep 2026 13:55:08 +0530 Subject: [PATCH 05/17] feat(rbac): gate workflow create on createPipeline (apps) Co-Authored-By: Claude Opus 4.8 --- .../app/workflow/AppWorkflowRestHandler.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/api/restHandler/app/workflow/AppWorkflowRestHandler.go b/api/restHandler/app/workflow/AppWorkflowRestHandler.go index bcad863c97..1471a50da7 100644 --- a/api/restHandler/app/workflow/AppWorkflowRestHandler.go +++ b/api/restHandler/app/workflow/AppWorkflowRestHandler.go @@ -102,7 +102,19 @@ func (handler AppWorkflowRestHandlerImpl) CreateAppWorkflow(w http.ResponseWrite token := r.Header.Get("token") //rbac block starts from here resourceName := handler.enforcerUtil.GetAppRBACNameByAppId(request.AppId) - ok := handler.enforcerUtil.CheckAppRbacForAppOrJob(token, resourceName, casbin.ActionCreate) + app, err := handler.pipelineBuilder.GetApp(request.AppId) + if err != nil { + handler.Logger.Errorw("error in fetching app for rbac", "err", err, "appId", request.AppId) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + // apps enforce the applications lifecycle action (createPipeline); jobs enforce the jobs resource only + var ok bool + if app.AppType == helper.Job { + ok = handler.enforcer.Enforce(token, casbin.ResourceJobs, casbin.ActionCreate, resourceName) + } else { + ok = handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionCreatePipeline, resourceName) + } if !ok { common.WriteJsonResp(w, err, "Unauthorized User", http.StatusForbidden) return From cef8bc0a1f69f4a88f75d9f26e8df0febd259b44 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Mon, 21 Sep 2026 13:55:22 +0530 Subject: [PATCH 06/17] feat(rbac): gate workflow delete on deletePipeline (apps) Co-Authored-By: Claude Opus 4.8 --- api/restHandler/app/workflow/AppWorkflowRestHandler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/restHandler/app/workflow/AppWorkflowRestHandler.go b/api/restHandler/app/workflow/AppWorkflowRestHandler.go index 1471a50da7..87632a506b 100644 --- a/api/restHandler/app/workflow/AppWorkflowRestHandler.go +++ b/api/restHandler/app/workflow/AppWorkflowRestHandler.go @@ -166,7 +166,7 @@ func (handler AppWorkflowRestHandlerImpl) DeleteAppWorkflow(w http.ResponseWrite //rbac block starts from here resourceName := handler.enforcerUtil.GetAppRBACNameByAppId(appId) workflowResourceName := handler.enforcerUtil.GetRbacObjectNameByAppIdAndWorkflow(appId, appWorkflow.Name) - ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionDelete, resourceName) + ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionDeletePipeline, resourceName) if !ok { ok = handler.enforcer.Enforce(token, casbin.ResourceJobs, casbin.ActionDelete, resourceName) && handler.enforcer.Enforce(token, casbin.ResourceWorkflow, casbin.ActionDelete, workflowResourceName) } From 03ab785df1ff5cc2d5d95ea1adddbeb2ff276bfc Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Mon, 21 Sep 2026 13:55:44 +0530 Subject: [PATCH 07/17] feat(rbac): gate bulk CD pipeline delete on deletePipeline Co-Authored-By: Claude Opus 4.8 --- api/restHandler/BulkUpdateRestHandler.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/restHandler/BulkUpdateRestHandler.go b/api/restHandler/BulkUpdateRestHandler.go index 782bddd7bb..8850ffe42b 100644 --- a/api/restHandler/BulkUpdateRestHandler.go +++ b/api/restHandler/BulkUpdateRestHandler.go @@ -461,7 +461,8 @@ func (handler BulkUpdateRestHandlerImpl) HandleCdPipelineBulkAction(w http.Respo //check to avoid same rbac matching multiple times if _, ok := appsHavingRbacChecked[impactedPipeline.App.AppName]; !ok { resourceName := handler.enforcerUtil.GetAppRBACName(impactedPipeline.App.AppName) - if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionUpdate, resourceName); !ok { + // bulk CD action only performs pipeline deletion (CD_BULK_DELETE); gate it as lifecycle delete + if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionDeletePipeline, resourceName); !ok { common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden) return } else { From 3e1417c7882ea7188005280f764f2dfa1c49a506 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Mon, 21 Sep 2026 13:56:04 +0530 Subject: [PATCH 08/17] feat(rbac): whitelist createPipeline/deletePipeline for applications Co-Authored-By: Claude Opus 4.8 --- .../sql/36704600_dev_admin_pipeline_lifecycle.down.sql | 4 ++++ scripts/sql/36704600_dev_admin_pipeline_lifecycle.up.sql | 8 ++++++++ 2 files changed, 12 insertions(+) create mode 100644 scripts/sql/36704600_dev_admin_pipeline_lifecycle.down.sql create mode 100644 scripts/sql/36704600_dev_admin_pipeline_lifecycle.up.sql diff --git a/scripts/sql/36704600_dev_admin_pipeline_lifecycle.down.sql b/scripts/sql/36704600_dev_admin_pipeline_lifecycle.down.sql new file mode 100644 index 0000000000..6e50adba04 --- /dev/null +++ b/scripts/sql/36704600_dev_admin_pipeline_lifecycle.down.sql @@ -0,0 +1,4 @@ +UPDATE rbac_policy_resource_detail +SET allowed_actions = array_remove(array_remove(allowed_actions, 'createPipeline'), 'deletePipeline'), + updated_on = now() +WHERE resource = 'applications'; diff --git a/scripts/sql/36704600_dev_admin_pipeline_lifecycle.up.sql b/scripts/sql/36704600_dev_admin_pipeline_lifecycle.up.sql new file mode 100644 index 0000000000..b1d0da2a68 --- /dev/null +++ b/scripts/sql/36704600_dev_admin_pipeline_lifecycle.up.sql @@ -0,0 +1,8 @@ +-- Increment 2: add createPipeline / deletePipeline to the applications resource whitelist so custom +-- roles can grant/withhold pipeline lifecycle independently of config actions. dev-admin omits them. +UPDATE rbac_policy_resource_detail +SET allowed_actions = array_cat(allowed_actions, ARRAY ['createPipeline','deletePipeline']), + updated_on = now() +WHERE resource = 'applications' + AND allowed_actions IS NOT NULL + AND NOT ('createPipeline' = ANY (allowed_actions)); From 46079f0f4320c25bfad77bb654dbcc48b0f0ca6a Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Mon, 21 Sep 2026 15:56:15 +0530 Subject: [PATCH 09/17] feat(rbac): gate CI pipeline add/delete via patch on create/deletePipeline Co-Authored-By: Claude Opus 4.8 --- .../configure/BuildPipelineRestHandler.go | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/api/restHandler/app/pipeline/configure/BuildPipelineRestHandler.go b/api/restHandler/app/pipeline/configure/BuildPipelineRestHandler.go index 6cfe2f1d85..6782e1c9b7 100644 --- a/api/restHandler/app/pipeline/configure/BuildPipelineRestHandler.go +++ b/api/restHandler/app/pipeline/configure/BuildPipelineRestHandler.go @@ -405,9 +405,27 @@ func (handler *PipelineConfigRestHandlerImpl) PatchCiPipelines(w http.ResponseWr return } - haveCiPatchAccess := handler.checkCiPatchAccess(token, resourceName, cdPipelines) - if !haveCiPatchAccess { - haveCiPatchAccess = handler.enforcer.Enforce(token, casbin.ResourceJobs, casbin.ActionCreate, resourceName) && handler.enforcer.Enforce(token, casbin.ResourceWorkflow, casbin.ActionCreate, workflowResourceName) + // Pipeline lifecycle (add/delete a CI pipeline) is gated by the dedicated lifecycle actions, + // independent of config/branch editing which keeps its existing access rules. Switch-CI is a + // creation flow and is treated as create. The jobs fallback is unchanged for all arms. + var haveCiPatchAccess bool + switch { + case patchRequest.Action == bean.CREATE || patchRequest.IsSwitchCiPipelineRequest(): + haveCiPatchAccess = handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionCreatePipeline, resourceName) + if !haveCiPatchAccess { + haveCiPatchAccess = handler.enforcer.Enforce(token, casbin.ResourceJobs, casbin.ActionCreate, resourceName) && handler.enforcer.Enforce(token, casbin.ResourceWorkflow, casbin.ActionCreate, workflowResourceName) + } + case patchRequest.Action == bean.DELETE: + haveCiPatchAccess = handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionDeletePipeline, resourceName) + if !haveCiPatchAccess { + haveCiPatchAccess = handler.enforcer.Enforce(token, casbin.ResourceJobs, casbin.ActionCreate, resourceName) && handler.enforcer.Enforce(token, casbin.ResourceWorkflow, casbin.ActionCreate, workflowResourceName) + } + default: + // UPDATE_PIPELINE / UPDATE_SOURCE: config/branch edit — existing access rules unchanged + haveCiPatchAccess = handler.checkCiPatchAccess(token, resourceName, cdPipelines) + if !haveCiPatchAccess { + haveCiPatchAccess = handler.enforcer.Enforce(token, casbin.ResourceJobs, casbin.ActionCreate, resourceName) && handler.enforcer.Enforce(token, casbin.ResourceWorkflow, casbin.ActionCreate, workflowResourceName) + } } if !haveCiPatchAccess { common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden) From 85c638719f5f36a36d77f0c6e31da86a67c9d389 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Mon, 21 Sep 2026 16:13:10 +0530 Subject: [PATCH 10/17] chore(rbac): bump dev-admin pipeline migration to 36904600 Avoids collision with develop's 36704600_variable_entity_mapping_index; aligns with enterprise 36904600. Co-Authored-By: Claude Opus 4.8 --- ...le.down.sql => 36904600_dev_admin_pipeline_lifecycle.down.sql} | 0 ...ecycle.up.sql => 36904600_dev_admin_pipeline_lifecycle.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename scripts/sql/{36704600_dev_admin_pipeline_lifecycle.down.sql => 36904600_dev_admin_pipeline_lifecycle.down.sql} (100%) rename scripts/sql/{36704600_dev_admin_pipeline_lifecycle.up.sql => 36904600_dev_admin_pipeline_lifecycle.up.sql} (100%) diff --git a/scripts/sql/36704600_dev_admin_pipeline_lifecycle.down.sql b/scripts/sql/36904600_dev_admin_pipeline_lifecycle.down.sql similarity index 100% rename from scripts/sql/36704600_dev_admin_pipeline_lifecycle.down.sql rename to scripts/sql/36904600_dev_admin_pipeline_lifecycle.down.sql diff --git a/scripts/sql/36704600_dev_admin_pipeline_lifecycle.up.sql b/scripts/sql/36904600_dev_admin_pipeline_lifecycle.up.sql similarity index 100% rename from scripts/sql/36704600_dev_admin_pipeline_lifecycle.up.sql rename to scripts/sql/36904600_dev_admin_pipeline_lifecycle.up.sql From 7ec202a0d22845ee7b082459b4fef50231f68d32 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Wed, 23 Sep 2026 13:07:42 +0530 Subject: [PATCH 11/17] chore(rbac): bump dev-admin pipeline migration to 37004800 (packet 48) develop added 36904600_add_ci_workflow_active_status_index (packet 46); move ours to the next free slot in the current packet (48). Co-Authored-By: Claude Opus 4.8 --- ...le.down.sql => 37004800_dev_admin_pipeline_lifecycle.down.sql} | 0 ...ecycle.up.sql => 37004800_dev_admin_pipeline_lifecycle.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename scripts/sql/{36904600_dev_admin_pipeline_lifecycle.down.sql => 37004800_dev_admin_pipeline_lifecycle.down.sql} (100%) rename scripts/sql/{36904600_dev_admin_pipeline_lifecycle.up.sql => 37004800_dev_admin_pipeline_lifecycle.up.sql} (100%) diff --git a/scripts/sql/36904600_dev_admin_pipeline_lifecycle.down.sql b/scripts/sql/37004800_dev_admin_pipeline_lifecycle.down.sql similarity index 100% rename from scripts/sql/36904600_dev_admin_pipeline_lifecycle.down.sql rename to scripts/sql/37004800_dev_admin_pipeline_lifecycle.down.sql diff --git a/scripts/sql/36904600_dev_admin_pipeline_lifecycle.up.sql b/scripts/sql/37004800_dev_admin_pipeline_lifecycle.up.sql similarity index 100% rename from scripts/sql/36904600_dev_admin_pipeline_lifecycle.up.sql rename to scripts/sql/37004800_dev_admin_pipeline_lifecycle.up.sql From cd80dd99648792180f6b6a521c6d0dfcf58677f2 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Mon, 5 Oct 2026 12:27:15 +0530 Subject: [PATCH 12/17] fix(rbac): close dev-admin pipeline/workflow bypasses from review Co-Authored-By: Claude Opus 4.8 --- api/restHandler/BatchOperationRestHandler.go | 2 +- api/restHandler/CoreAppRestHandler.go | 2 +- .../app/workflow/AppWorkflowRestHandler.go | 14 ++++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/api/restHandler/BatchOperationRestHandler.go b/api/restHandler/BatchOperationRestHandler.go index c1f3adb535..9f5c53caa6 100644 --- a/api/restHandler/BatchOperationRestHandler.go +++ b/api/restHandler/BatchOperationRestHandler.go @@ -100,7 +100,7 @@ func (handler BatchOperationRestHandlerImpl) Operate(w http.ResponseWriter, r *h return } rbacString := handler.enforcerUtil.GetProjectAdminRBACNameBYAppName(*workflow.Destination.App) - if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionCreate, rbacString); !ok { + if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionCreatePipeline, rbacString); !ok { common.WriteJsonResp(w, err, "Unauthorized User", http.StatusForbidden) return } diff --git a/api/restHandler/CoreAppRestHandler.go b/api/restHandler/CoreAppRestHandler.go index bbe991a5b3..fdf25892bf 100644 --- a/api/restHandler/CoreAppRestHandler.go +++ b/api/restHandler/CoreAppRestHandler.go @@ -2278,7 +2278,7 @@ func (handler CoreAppRestHandlerImpl) CreateAppWorkflow(w http.ResponseWriter, r createAppRequest.AppId = app.AppId object := fmt.Sprintf("%s/%s", app.ProjectName, app.AppName) // with admin roles, you have to access for all the apps of the project to create new app. (admin or manager with specific app permission can't create app.) - if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionCreate, object); !ok { + if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionCreatePipeline, object); !ok { common.WriteJsonResp(w, err, "Unauthorized User", http.StatusForbidden) return } diff --git a/api/restHandler/app/workflow/AppWorkflowRestHandler.go b/api/restHandler/app/workflow/AppWorkflowRestHandler.go index 87632a506b..8669db140d 100644 --- a/api/restHandler/app/workflow/AppWorkflowRestHandler.go +++ b/api/restHandler/app/workflow/AppWorkflowRestHandler.go @@ -166,9 +166,19 @@ func (handler AppWorkflowRestHandlerImpl) DeleteAppWorkflow(w http.ResponseWrite //rbac block starts from here resourceName := handler.enforcerUtil.GetAppRBACNameByAppId(appId) workflowResourceName := handler.enforcerUtil.GetRbacObjectNameByAppIdAndWorkflow(appId, appWorkflow.Name) - ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionDeletePipeline, resourceName) - if !ok { + app, err := handler.pipelineBuilder.GetApp(appId) + if err != nil { + handler.Logger.Errorw("service err, GetApp", "err", err, "appId", appId) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + var ok bool + if app.AppType == helper.Job { + // jobs: enforce the jobs resource only (no applications OR), symmetric with CreateAppWorkflow ok = handler.enforcer.Enforce(token, casbin.ResourceJobs, casbin.ActionDelete, resourceName) && handler.enforcer.Enforce(token, casbin.ResourceWorkflow, casbin.ActionDelete, workflowResourceName) + } else { + // devtron apps: workflow delete is pipeline lifecycle + ok = handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionDeletePipeline, resourceName) } if !ok { common.WriteJsonResp(w, err, "Unauthorized User", http.StatusForbidden) From 1e68ac0383998796d33ef2b2f9f200ed7da7fb3a Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Mon, 5 Oct 2026 12:37:34 +0530 Subject: [PATCH 13/17] fix(rbac): match CI patch DELETE before create/linked/switch to prevent delete via switch field Co-Authored-By: Claude Opus 4.8 --- .../app/pipeline/configure/BuildPipelineRestHandler.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/restHandler/app/pipeline/configure/BuildPipelineRestHandler.go b/api/restHandler/app/pipeline/configure/BuildPipelineRestHandler.go index 6782e1c9b7..713236f092 100644 --- a/api/restHandler/app/pipeline/configure/BuildPipelineRestHandler.go +++ b/api/restHandler/app/pipeline/configure/BuildPipelineRestHandler.go @@ -410,13 +410,13 @@ func (handler *PipelineConfigRestHandlerImpl) PatchCiPipelines(w http.ResponseWr // creation flow and is treated as create. The jobs fallback is unchanged for all arms. var haveCiPatchAccess bool switch { - case patchRequest.Action == bean.CREATE || patchRequest.IsSwitchCiPipelineRequest(): - haveCiPatchAccess = handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionCreatePipeline, resourceName) + case patchRequest.Action == bean.DELETE: + haveCiPatchAccess = handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionDeletePipeline, resourceName) if !haveCiPatchAccess { haveCiPatchAccess = handler.enforcer.Enforce(token, casbin.ResourceJobs, casbin.ActionCreate, resourceName) && handler.enforcer.Enforce(token, casbin.ResourceWorkflow, casbin.ActionCreate, workflowResourceName) } - case patchRequest.Action == bean.DELETE: - haveCiPatchAccess = handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionDeletePipeline, resourceName) + case patchRequest.Action == bean.CREATE || patchRequest.IsSwitchCiPipelineRequest(): + haveCiPatchAccess = handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionCreatePipeline, resourceName) if !haveCiPatchAccess { haveCiPatchAccess = handler.enforcer.Enforce(token, casbin.ResourceJobs, casbin.ActionCreate, resourceName) && handler.enforcer.Enforce(token, casbin.ResourceWorkflow, casbin.ActionCreate, workflowResourceName) } From af3f3db1b7d4bde1ad55a3845ded004e6ba37312 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Tue, 6 Oct 2026 16:11:03 +0530 Subject: [PATCH 14/17] fix(rbac): gate CoreApp CreateAppWorkflow on createPipeline only when a workflow is created (keep config-only on create) Co-Authored-By: Claude Opus 4.8 --- api/restHandler/CoreAppRestHandler.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/api/restHandler/CoreAppRestHandler.go b/api/restHandler/CoreAppRestHandler.go index fdf25892bf..b50d215aff 100644 --- a/api/restHandler/CoreAppRestHandler.go +++ b/api/restHandler/CoreAppRestHandler.go @@ -2277,8 +2277,13 @@ func (handler CoreAppRestHandlerImpl) CreateAppWorkflow(w http.ResponseWriter, r } createAppRequest.AppId = app.AppId object := fmt.Sprintf("%s/%s", app.ProjectName, app.AppName) - // with admin roles, you have to access for all the apps of the project to create new app. (admin or manager with specific app permission can't create app.) - if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionCreatePipeline, object); !ok { + // Creating a workflow is pipeline lifecycle (createPipeline); a config-only request + // (environmentOverrides with no workflow) stays on create so dev-admin keeps config-edit access. + wfCreateAction := casbin.ActionCreate + if len(createAppRequest.AppWorkflows) > 0 { + wfCreateAction = casbin.ActionCreatePipeline + } + if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, wfCreateAction, object); !ok { common.WriteJsonResp(w, err, "Unauthorized User", http.StatusForbidden) return } From d4761c0bcf0d019b03da3b6cf62cada3ff12cc16 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Tue, 6 Oct 2026 16:16:10 +0530 Subject: [PATCH 15/17] fix(rbac): gate non-cascade CD pipeline delete (force=true) on deletePipeline Co-Authored-By: Claude Opus 4.8 --- .../app/pipeline/configure/PipelineConfigRestHandler.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/restHandler/app/pipeline/configure/PipelineConfigRestHandler.go b/api/restHandler/app/pipeline/configure/PipelineConfigRestHandler.go index e683c2cfcb..2f01e9dad1 100644 --- a/api/restHandler/app/pipeline/configure/PipelineConfigRestHandler.go +++ b/api/restHandler/app/pipeline/configure/PipelineConfigRestHandler.go @@ -336,7 +336,11 @@ func (handler *PipelineConfigRestHandlerImpl) DeleteACDAppWithNonCascade(w http. } // rbac enforcer applying resourceName := handler.enforcerUtil.GetAppRBACName(app.AppName) - if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionGet, resourceName); !ok { + appAction := casbin.ActionGet + if forceDelete { // force=true deletes the CD pipeline -> pipeline lifecycle + appAction = casbin.ActionDeletePipeline + } + if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, appAction, resourceName); !ok { common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden) return } From 18fda2e527f7c2bab1b1d8984867d34eabf9cd72 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Tue, 6 Oct 2026 16:49:17 +0530 Subject: [PATCH 16/17] fix(rbac): lowercase dev-admin lifecycle action constants to match casbin store Casbin normalizes policy actions to lowercase on store (AddPolicy -> strings.ToLower) while enforcement passes the action unchanged. The mixed-case createApp/deleteApp/createPipeline/deletePipeline constants therefore never matched an explicit custom-role grant. Lowercase the constant values and the whitelist labels; the Release-1 createApp/deleteApp labels are normalized inside this PR's 37004800 migration via array_replace so the already-shipped 36504600 migration is left untouched. admin (*) and dev-admin (omission) are unaffected; this only fixes future explicit custom-role grants of these four actions. Co-Authored-By: Claude Opus 4.8 --- pkg/auth/authorisation/casbin/rbacpolicy.go | 12 +++++++---- ...4800_dev_admin_pipeline_lifecycle.down.sql | 13 +++++++++++- ...004800_dev_admin_pipeline_lifecycle.up.sql | 21 ++++++++++++++++--- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/pkg/auth/authorisation/casbin/rbacpolicy.go b/pkg/auth/authorisation/casbin/rbacpolicy.go index ab1d7c5066..c4252bfbde 100644 --- a/pkg/auth/authorisation/casbin/rbacpolicy.go +++ b/pkg/auth/authorisation/casbin/rbacpolicy.go @@ -62,13 +62,17 @@ const ( // ActionCreateApp and ActionDeleteApp gate the application-entity lifecycle (create/delete an app), // decoupled from ActionCreate/ActionDelete which also gate pipeline/workflow/config operations. // admin/manager/super-admin keep app lifecycle via their wildcard (`*`) action policies. - ActionCreateApp = "createApp" - ActionDeleteApp = "deleteApp" + // Values are lowercase: the casbin store normalizes policy actions to lowercase (AddPolicy -> + // strings.ToLower), while enforcement passes the action unchanged, so the constant must already be + // lowercase to match a stored grant. + ActionCreateApp = "createapp" + ActionDeleteApp = "deleteapp" // ActionCreatePipeline and ActionDeletePipeline gate CI/CD pipeline and workflow lifecycle // (create/delete), decoupled from ActionCreate/ActionUpdate/ActionDelete which continue to gate // config editing. admin/manager/super-admin keep lifecycle via their wildcard (`*`) action policies. - ActionCreatePipeline = "createPipeline" - ActionDeletePipeline = "deletePipeline" + // Lowercase for the same store-normalization reason as ActionCreateApp above. + ActionCreatePipeline = "createpipeline" + ActionDeletePipeline = "deletepipeline" ClusterResourceRegex = "%s/%s" // {cluster}/{namespace} ClusterObjectRegex = "%s/%s/%s" // {groupName}/{kindName}/{objectName} diff --git a/scripts/sql/37004800_dev_admin_pipeline_lifecycle.down.sql b/scripts/sql/37004800_dev_admin_pipeline_lifecycle.down.sql index 6e50adba04..02aeed7416 100644 --- a/scripts/sql/37004800_dev_admin_pipeline_lifecycle.down.sql +++ b/scripts/sql/37004800_dev_admin_pipeline_lifecycle.down.sql @@ -1,4 +1,15 @@ +-- Reverse Increment 2: remove pipeline lifecycle actions and restore Release-1 lifecycle actions to +-- their original mixed-case form (as 36504600 left them), so down returns to the pre-migration state. + +-- 1) remove pipeline lifecycle actions UPDATE rbac_policy_resource_detail -SET allowed_actions = array_remove(array_remove(allowed_actions, 'createPipeline'), 'deletePipeline'), +SET allowed_actions = array_remove(array_remove(allowed_actions, 'createpipeline'), 'deletepipeline'), updated_on = now() WHERE resource = 'applications'; + +-- 2) restore Release-1 application lifecycle actions to mixed case +UPDATE rbac_policy_resource_detail +SET allowed_actions = array_replace(array_replace(allowed_actions, 'createapp', 'createApp'), 'deleteapp', 'deleteApp'), + updated_on = now() +WHERE resource = 'applications' + AND allowed_actions IS NOT NULL; diff --git a/scripts/sql/37004800_dev_admin_pipeline_lifecycle.up.sql b/scripts/sql/37004800_dev_admin_pipeline_lifecycle.up.sql index b1d0da2a68..719e229dc8 100644 --- a/scripts/sql/37004800_dev_admin_pipeline_lifecycle.up.sql +++ b/scripts/sql/37004800_dev_admin_pipeline_lifecycle.up.sql @@ -1,8 +1,23 @@ --- Increment 2: add createPipeline / deletePipeline to the applications resource whitelist so custom +-- Increment 2: add createpipeline / deletepipeline to the applications resource whitelist so custom -- roles can grant/withhold pipeline lifecycle independently of config actions. dev-admin omits them. +-- +-- Action values are lowercase on purpose: the casbin store normalizes policy actions to lowercase on +-- AddPolicy (strings.ToLower) while enforcement passes the action unchanged, so a mixed-case whitelist +-- label would never match the stored grant. This migration also normalizes the Release-1 lifecycle +-- actions (createApp/deleteApp, added by 36504600) to lowercase here rather than editing that already +-- shipped migration, so the whitelist is internally consistent on both fresh and upgraded installs. + +-- 1) normalize Release-1 application lifecycle actions to lowercase (no-op if already lowercase) UPDATE rbac_policy_resource_detail -SET allowed_actions = array_cat(allowed_actions, ARRAY ['createPipeline','deletePipeline']), +SET allowed_actions = array_replace(array_replace(allowed_actions, 'createApp', 'createapp'), 'deleteApp', 'deleteapp'), + updated_on = now() +WHERE resource = 'applications' + AND allowed_actions IS NOT NULL; + +-- 2) add pipeline lifecycle actions (lowercase) +UPDATE rbac_policy_resource_detail +SET allowed_actions = array_cat(allowed_actions, ARRAY ['createpipeline','deletepipeline']), updated_on = now() WHERE resource = 'applications' AND allowed_actions IS NOT NULL - AND NOT ('createPipeline' = ANY (allowed_actions)); + AND NOT ('createpipeline' = ANY (allowed_actions)); From 6f1ca8ad1e738bdf1b6a4251392504aeaa3bda65 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Thu, 15 Oct 2026 10:41:57 +0530 Subject: [PATCH 17/17] chore(rbac): bump dev-admin pipeline migration 37004800 -> 37704800 above develop max develop added migrations up to 37504600; the runner only applies versions above the current schema version, so a 37004800 migration would be skipped on envs already at 37504600. Bumped to 37704800 (packet 48) to stay above develop's max and keep both repos aligned. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Se9N8bnyDEitxXB69Pt55V --- ...le.down.sql => 37704800_dev_admin_pipeline_lifecycle.down.sql} | 0 ...ecycle.up.sql => 37704800_dev_admin_pipeline_lifecycle.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename scripts/sql/{37004800_dev_admin_pipeline_lifecycle.down.sql => 37704800_dev_admin_pipeline_lifecycle.down.sql} (100%) rename scripts/sql/{37004800_dev_admin_pipeline_lifecycle.up.sql => 37704800_dev_admin_pipeline_lifecycle.up.sql} (100%) diff --git a/scripts/sql/37004800_dev_admin_pipeline_lifecycle.down.sql b/scripts/sql/37704800_dev_admin_pipeline_lifecycle.down.sql similarity index 100% rename from scripts/sql/37004800_dev_admin_pipeline_lifecycle.down.sql rename to scripts/sql/37704800_dev_admin_pipeline_lifecycle.down.sql diff --git a/scripts/sql/37004800_dev_admin_pipeline_lifecycle.up.sql b/scripts/sql/37704800_dev_admin_pipeline_lifecycle.up.sql similarity index 100% rename from scripts/sql/37004800_dev_admin_pipeline_lifecycle.up.sql rename to scripts/sql/37704800_dev_admin_pipeline_lifecycle.up.sql