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
33f5764
fix(security): enforce caller RBAC in API token webhook endpoint
claude Sep 1, 2026
4657d97
chore: address SonarCloud finding in GetAllApiTokensForWebhook
claude Sep 1, 2026
5566030
fix(security): don't leak more-privileged tokens to less-privileged c…
claude Sep 1, 2026
73b7d2a
chore: address SonarCloud param-grouping findings in ApiTokenRestHandler
claude Sep 1, 2026
ba2f3b9
fix(security): simplify to a straightforward super-admin gate
claude Sep 1, 2026
5d2c905
chore: address SonarCloud finding in super-admin gate check
claude Sep 1, 2026
e02f2be
Merge pull request #7016 from devtron-labs/fix/api-token-webhook-rbac…
Shivam-nagar23 Sep 1, 2026
5e5ebdc
feat: decouple app lifecycle actions; add indexes to improve CI query…
Shivam-nagar23 Sep 1, 2026
e3f0358
Merge branch 'main' into feat/dev-admin-app-lifecycle
Shivam-nagar23 Sep 2, 2026
4b2bf38
fix(user): scope filter-based bulk delete to match listing filters
claude Sep 4, 2026
418ccc7
Merge pull request #7019 from devtron-labs/feat/dev-admin-app-lifecycle
Shivam-nagar23 Sep 7, 2026
a514e03
Merge branch 'main' into fix/bulk-delete-filter-scope
Shivam-nagar23 Sep 7, 2026
5916f39
Merge pull request #7021 from devtron-labs/fix/bulk-delete-filter-scope
Shivam-nagar23 Sep 7, 2026
1679300
feat: resolve RBAC inconsistencies for helm apps with unique identifiers
Shivam-nagar23 Sep 11, 2026
81c8bcf
Conditions saved with the plugin definition (plugin_step_condition) a…
prakash100198 Sep 14, 2026
a463577
Merge pull request #7034 from devtron-labs/fix-link-app-rbac
Shivam-nagar23 Sep 15, 2026
36dc113
Merge branch 'main' into support-trigger-conditions-pipeline-level-oss
prakash100198 Sep 15, 2026
72dcd49
Merge pull request #7035 from devtron-labs/support-trigger-conditions…
prakash100198 Sep 15, 2026
bbb3ec7
feat: RBAC support for external argo/flux apps (#7014)
vivekanandan-devtron Sep 16, 2026
1bb65f6
Merge remote-tracking branch 'origin/main' into main-sync-develop-16sep
vivekanandan-devtron Sep 16, 2026
5eee92f
chore: rename migration files to correct sequence
vivekanandan-devtron Sep 16, 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
10 changes: 9 additions & 1 deletion api/apiToken/ApiTokenRestHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,15 @@ func (impl ApiTokenRestHandlerImpl) GetAllApiTokensForWebhook(w http.ResponseWri
return
}

// handle super-admin RBAC
// handle super-admin RBAC - this endpoint's only caller is the dashboard's
// webhook config modal, which is itself super-admin-only, so gate the
// whole endpoint the same way the other handlers in this file do.
token := r.Header.Get("token")
if !impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionUpdate, "*") {
common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden)
return
}

v := r.URL.Query()
projectName := v.Get("projectName")
environmentName := v.Get("environmentName")
Expand Down
50 changes: 38 additions & 12 deletions api/argoApplication/ArgoApplicationRestHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import (
"errors"
"github.com/devtron-labs/devtron/api/restHandler/common"
"github.com/devtron-labs/devtron/pkg/argoApplication"
"github.com/devtron-labs/devtron/pkg/argoApplication/bean"
"github.com/devtron-labs/devtron/pkg/argoApplication/read"
"github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin"
"github.com/devtron-labs/devtron/util/rbac"
"go.uber.org/zap"
"net/http"
"strconv"
Expand All @@ -39,26 +41,24 @@ type ArgoApplicationRestHandlerImpl struct {
readService read.ArgoApplicationReadService
logger *zap.SugaredLogger
enforcer casbin.Enforcer
enforcerUtilGitOps rbac.EnforcerUtilGitOps
}

func NewArgoApplicationRestHandlerImpl(argoApplicationService argoApplication.ArgoApplicationService,
readService read.ArgoApplicationReadService, logger *zap.SugaredLogger, enforcer casbin.Enforcer) *ArgoApplicationRestHandlerImpl {
readService read.ArgoApplicationReadService, logger *zap.SugaredLogger, enforcer casbin.Enforcer,
enforcerUtilGitOps rbac.EnforcerUtilGitOps) *ArgoApplicationRestHandlerImpl {
return &ArgoApplicationRestHandlerImpl{
argoApplicationService: argoApplicationService,
readService: readService,
logger: logger,
enforcer: enforcer,
enforcerUtilGitOps: enforcerUtilGitOps,
}

}

func (handler *ArgoApplicationRestHandlerImpl) ListApplications(w http.ResponseWriter, r *http.Request) {
// handle super-admin RBAC
token := r.Header.Get("token")
if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); !ok {
common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden)
return
}
v := r.URL.Query()
clusterIdString := v.Get("clusterIds")
var clusterIds []int
Expand All @@ -80,16 +80,34 @@ func (handler *ArgoApplicationRestHandlerImpl) ListApplications(w http.ResponseW
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
return
}
common.WriteJsonResp(w, nil, resp, http.StatusOK)
// RBAC enforcer applying: filter the listing to the applications the caller may see.
// Batched rather than a per-app Enforce loop; an app whose object cannot be built is dropped.
objects := make([]string, 0, len(resp))
objectByApp := make(map[*bean.ArgoApplicationListDto]string, len(resp))
for _, app := range resp {
object := handler.enforcerUtilGitOps.GetExternalGitOpsAppObjectByClusterName(app.ClusterName, app.Namespace, app.Name)
if len(object) == 0 {
continue
}
objectByApp[app] = object
objects = append(objects, object)
}
authorisedObjects := make(map[string]bool)
if len(objects) > 0 {
authorisedObjects = handler.enforcer.EnforceInBatch(token, casbin.ResourceArgoApp, casbin.ActionGet, objects)
}
authorisedApps := make([]*bean.ArgoApplicationListDto, 0, len(resp))
for _, app := range resp {
if object, ok := objectByApp[app]; ok && authorisedObjects[strings.ToLower(object)] {
authorisedApps = append(authorisedApps, app)
}
}
//RBAC enforcer Ends
common.WriteJsonResp(w, nil, authorisedApps, http.StatusOK)
}

func (handler *ArgoApplicationRestHandlerImpl) GetApplicationDetail(w http.ResponseWriter, r *http.Request) {
// handle super-admin RBAC
token := r.Header.Get("token")
if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); !ok {
common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden)
return
}
ctx := r.Context()
ctx = context.WithValue(ctx, "token", token)

Expand All @@ -108,6 +126,14 @@ func (handler *ArgoApplicationRestHandlerImpl) GetApplicationDetail(w http.Respo
return
}
}
// RBAC enforcer applying
object := handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(clusterId, namespace, resourceName)
if len(object) == 0 || !handler.enforcer.Enforce(token, casbin.ResourceArgoApp, casbin.ActionGet, object) {
common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden)
return
}
//RBAC enforcer Ends

resp, err := handler.readService.GetAppDetailEA(ctx, resourceName, namespace, clusterId)
if err != nil {
handler.logger.Errorw("error in getting argo application app detail", "err", err, "resourceName", resourceName, "clusterId", clusterId)
Expand Down
33 changes: 30 additions & 3 deletions api/auth/user/UserRestHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ package user
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"

util2 "github.com/devtron-labs/devtron/api/auth/user/util"
"github.com/devtron-labs/devtron/pkg/auth/user/helper"
"github.com/devtron-labs/devtron/util/commonEnforcementFunctionsUtil"
"github.com/gorilla/schema"
"net/http"
"strconv"
"strings"

"github.com/devtron-labs/devtron/api/restHandler/common"
"github.com/devtron-labs/devtron/internal/util"
Expand All @@ -38,6 +39,16 @@ import (
"gopkg.in/go-playground/validator.v9"
)

const (
resultKeyArgoAppAccess = "hasArgoAppAccess"
resultKeyFluxAppAccess = "hasFluxAppAccess"
)

var gitOpsAccessResultKeyByResource = map[string]string{
casbin.ResourceArgoApp: resultKeyArgoAppAccess,
casbin.ResourceFluxApp: resultKeyFluxAppAccess,
}

type UserRestHandler interface {
CreateUser(w http.ResponseWriter, r *http.Request)
UpdateUser(w http.ResponseWriter, r *http.Request)
Expand Down Expand Up @@ -795,9 +806,25 @@ func (handler UserRestHandlerImpl) CheckUserRoles(w http.ResponseWriter, r *http
result := make(map[string]interface{})
result["roles"] = roles
result["superAdmin"] = false
result[resultKeyArgoAppAccess] = false
result[resultKeyFluxAppAccess] = false
for _, item := range roles {
if item == bean2.SUPERADMIN {
result["superAdmin"] = true
result[resultKeyArgoAppAccess] = true
result[resultKeyFluxAppAccess] = true
continue
}

roleFragments := strings.Split(item, "_")
resourceActionFragment := strings.Split(roleFragments[0], ":")

if len(resourceActionFragment) < 2 {
continue
}

if resultKey, ok := gitOpsAccessResultKeyByResource[resourceActionFragment[0]]; ok {
result[resultKey] = true
}
}
common.WriteJsonResp(w, err, result, http.StatusOK)
Expand Down
29 changes: 20 additions & 9 deletions api/fluxApplication/FluxApplicationRestHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/devtron-labs/devtron/api/restHandler/common"
"github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin"
clientErrors "github.com/devtron-labs/devtron/pkg/errors"
"github.com/devtron-labs/devtron/util/rbac"
"github.com/devtron-labs/devtron/pkg/fluxApplication"
"github.com/gorilla/mux"
"go.uber.org/zap"
Expand All @@ -20,26 +21,34 @@ type FluxApplicationRestHandlerImpl struct {
fluxApplicationService fluxApplication.FluxApplicationService
logger *zap.SugaredLogger
enforcer casbin.Enforcer
enforcerUtilGitOps rbac.EnforcerUtilGitOps
}

func NewFluxApplicationRestHandlerImpl(fluxApplicationService fluxApplication.FluxApplicationService,
logger *zap.SugaredLogger, enforcer casbin.Enforcer) *FluxApplicationRestHandlerImpl {
logger *zap.SugaredLogger, enforcer casbin.Enforcer,
enforcerUtilGitOps rbac.EnforcerUtilGitOps) *FluxApplicationRestHandlerImpl {
return &FluxApplicationRestHandlerImpl{
fluxApplicationService: fluxApplicationService,
logger: logger,
enforcer: enforcer,
enforcerUtilGitOps: enforcerUtilGitOps,
}

}

// checkFluxAppAuth builds the RBAC object from the app identity and enforces on it. Passed into
// the service because the app list is streamed and cannot be filtered after the fact.
func (handler *FluxApplicationRestHandlerImpl) checkFluxAppAuth(token string, clusterName string, namespace string, appName string) bool {
object := handler.enforcerUtilGitOps.GetExternalGitOpsAppObjectByClusterName(clusterName, namespace, appName)
if len(object) == 0 {
return false
}
return handler.enforcer.Enforce(token, casbin.ResourceFluxApp, casbin.ActionGet, object)
}

func (handler *FluxApplicationRestHandlerImpl) ListFluxApplications(w http.ResponseWriter, r *http.Request) {

//handle super-admin RBAC
token := r.Header.Get("token")
if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); !ok {
common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden)
return
}
v := r.URL.Query()
clusterIdString := v.Get("clusterIds")
var clusterIds []int
Expand All @@ -59,7 +68,7 @@ func (handler *FluxApplicationRestHandlerImpl) ListFluxApplications(w http.Respo
return
}
handler.logger.Debugw("extracted ClusterIds successfully ", "clusterIds", clusterIds)
handler.fluxApplicationService.ListFluxApplications(r.Context(), clusterIds, noStream, w)
handler.fluxApplicationService.ListFluxApplications(r.Context(), clusterIds, noStream, w, token, handler.checkFluxAppAuth)
}

func (handler *FluxApplicationRestHandlerImpl) GetApplicationDetail(w http.ResponseWriter, r *http.Request) {
Expand All @@ -76,12 +85,14 @@ func (handler *FluxApplicationRestHandlerImpl) GetApplicationDetail(w http.Respo
return
}

// handle super-admin RBAC
// RBAC enforcer applying
token := r.Header.Get("token")
if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); !ok {
object := handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(appIdentifier.ClusterId, appIdentifier.Namespace, appIdentifier.Name)
if len(object) == 0 || !handler.enforcer.Enforce(token, casbin.ResourceFluxApp, casbin.ActionGet, object) {
common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden)
return
}
//RBAC enforcer Ends

res, err := handler.fluxApplicationService.GetFluxAppDetail(r.Context(), appIdentifier)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions api/helm-app/wire_helmApp.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ var HelmAppWireSet = wire.NewSet(
gRPC.GetConfig,
rbac.NewEnforcerUtilHelmImpl,
wire.Bind(new(rbac.EnforcerUtilHelm), new(*rbac.EnforcerUtilHelmImpl)),

rbac.NewEnforcerUtilGitOpsImpl,
wire.Bind(new(rbac.EnforcerUtilGitOps), new(*rbac.EnforcerUtilGitOpsImpl)),
)
Loading
Loading