Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3853f15
feat(rbac): add createPipeline/deletePipeline actions
Shivam-nagar23 Sep 21, 2026
f54dc8e
feat(rbac): gate CI pipeline create on createPipeline
Shivam-nagar23 Sep 21, 2026
d9cb238
feat(rbac): gate CD pipeline create on createPipeline
Shivam-nagar23 Sep 21, 2026
9fd8217
fix(rbac): gate CD_CREATE via PatchCdPipeline on createPipeline
Shivam-nagar23 Sep 21, 2026
1dcfb97
feat(rbac): gate workflow create on createPipeline (apps)
Shivam-nagar23 Sep 21, 2026
cef8bc0
feat(rbac): gate workflow delete on deletePipeline (apps)
Shivam-nagar23 Sep 21, 2026
03ab785
feat(rbac): gate bulk CD pipeline delete on deletePipeline
Shivam-nagar23 Sep 21, 2026
3e1417c
feat(rbac): whitelist createPipeline/deletePipeline for applications
Shivam-nagar23 Sep 21, 2026
46079f0
feat(rbac): gate CI pipeline add/delete via patch on create/deletePip…
Shivam-nagar23 Sep 21, 2026
85c6387
chore(rbac): bump dev-admin pipeline migration to 36904600
Shivam-nagar23 Sep 21, 2026
7ec202a
chore(rbac): bump dev-admin pipeline migration to 37004800 (packet 48)
Shivam-nagar23 Sep 23, 2026
cd80dd9
fix(rbac): close dev-admin pipeline/workflow bypasses from review
Shivam-nagar23 Oct 5, 2026
1e68ac0
fix(rbac): match CI patch DELETE before create/linked/switch to preve…
Shivam-nagar23 Oct 5, 2026
b7e7c1d
Merge branch 'develop' into feat/dev-admin-pipeline-lifecycle
Shivam-nagar23 Oct 5, 2026
af3f3db
fix(rbac): gate CoreApp CreateAppWorkflow on createPipeline only when…
Shivam-nagar23 Oct 6, 2026
d4761c0
fix(rbac): gate non-cascade CD pipeline delete (force=true) on delete…
Shivam-nagar23 Oct 6, 2026
18fda2e
fix(rbac): lowercase dev-admin lifecycle action constants to match ca…
Shivam-nagar23 Oct 6, 2026
2215491
Merge branch 'develop' into feat/dev-admin-pipeline-lifecycle
Shivam-nagar23 Oct 15, 2026
e739e08
Merge remote-tracking branch 'origin/develop' into feat/dev-admin-pip…
Shivam-nagar23 Oct 15, 2026
6f1ca8a
chore(rbac): bump dev-admin pipeline migration 37004800 -> 37704800 a…
Shivam-nagar23 Oct 15, 2026
875d18f
Merge remote-tracking branch 'origin/feat/dev-admin-pipeline-lifecycl…
Shivam-nagar23 Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/restHandler/BatchOperationRestHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment thread
Shivam-nagar23 marked this conversation as resolved.
common.WriteJsonResp(w, err, "Unauthorized User", http.StatusForbidden)
return
}
Expand Down
3 changes: 2 additions & 1 deletion api/restHandler/BulkUpdateRestHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,8 @@
//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 {

Check warning on line 465 in api/restHandler/BulkUpdateRestHandler.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unnecessary variable declaration and use the expression directly in the condition.

See more on https://sonarcloud.io/project/issues?id=devtron-labs_devtron&issues=AaDDnY3De8_wdapXQVFR&open=AaDDnY3De8_wdapXQVFR&pullRequest=7043
common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden)
return
} else {
Expand Down
9 changes: 7 additions & 2 deletions api/restHandler/CoreAppRestHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2277,8 +2277,13 @@
}
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 {
// 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 {

Check warning on line 2286 in api/restHandler/CoreAppRestHandler.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unnecessary variable declaration and use the expression directly in the condition.

See more on https://sonarcloud.io/project/issues?id=devtron-labs_devtron&issues=AaEQ0YEMyr6HxMYFtdip&open=AaEQ0YEMyr6HxMYFtdip&pullRequest=7043
Comment thread
Shivam-nagar23 marked this conversation as resolved.
common.WriteJsonResp(w, err, "Unauthorized User", http.StatusForbidden)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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.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.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)
}
Comment thread
Shivam-nagar23 marked this conversation as resolved.
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)
}
}
Comment thread
Shivam-nagar23 marked this conversation as resolved.
if !haveCiPatchAccess {
common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@

// 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 {

Check warning on line 236 in api/restHandler/app/pipeline/configure/DeploymentPipelineRestHandler.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unnecessary variable declaration and use the expression directly in the condition.

See more on https://sonarcloud.io/project/issues?id=devtron-labs_devtron&issues=AaDDnYwhe8_wdapXQVFP&open=AaDDnYwhe8_wdapXQVFP&pullRequest=7043
common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden)
return
}
Expand Down Expand Up @@ -367,13 +367,23 @@
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
Comment thread
Shivam-nagar23 marked this conversation as resolved.
}
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 {

Check warning on line 380 in api/restHandler/app/pipeline/configure/DeploymentPipelineRestHandler.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unnecessary variable declaration and use the expression directly in the condition.

See more on https://sonarcloud.io/project/issues?id=devtron-labs_devtron&issues=AaDDnYwhe8_wdapXQVFQ&open=AaDDnYwhe8_wdapXQVFQ&pullRequest=7043
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,11 @@
}
// 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 {

Check warning on line 343 in api/restHandler/app/pipeline/configure/PipelineConfigRestHandler.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unnecessary variable declaration and use the expression directly in the condition.

See more on https://sonarcloud.io/project/issues?id=devtron-labs_devtron&issues=AaEQ19-ZBv0rcil6imYR&open=AaEQ19-ZBv0rcil6imYR&pullRequest=7043
common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden)
return
}
Expand Down
28 changes: 25 additions & 3 deletions api/restHandler/app/workflow/AppWorkflowRestHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -154,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.ActionDelete, 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)
Expand Down
13 changes: 11 additions & 2 deletions pkg/auth/authorisation/casbin/rbacpolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +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.
// 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}
Expand Down
15 changes: 15 additions & 0 deletions scripts/sql/37704800_dev_admin_pipeline_lifecycle.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +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'),
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;
23 changes: 23 additions & 0 deletions scripts/sql/37704800_dev_admin_pipeline_lifecycle.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- 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_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));
Loading