diff --git a/api/argoApplication/ArgoApplicationRestHandler.go b/api/argoApplication/ArgoApplicationRestHandler.go index edcbabdca9..5b816db2cd 100644 --- a/api/argoApplication/ArgoApplicationRestHandler.go +++ b/api/argoApplication/ArgoApplicationRestHandler.go @@ -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" @@ -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 @@ -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) @@ -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) diff --git a/api/auth/user/UserRestHandler.go b/api/auth/user/UserRestHandler.go index 37188c14bd..9a5e1a9705 100644 --- a/api/auth/user/UserRestHandler.go +++ b/api/auth/user/UserRestHandler.go @@ -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" @@ -795,10 +796,31 @@ func (handler UserRestHandlerImpl) CheckUserRoles(w http.ResponseWriter, r *http result := make(map[string]interface{}) result["roles"] = roles result["superAdmin"] = false + result["hasArgoAppAccess"] = false + result["hasFluxAppAccess"] = false for _, item := range roles { if item == bean2.SUPERADMIN { result["superAdmin"] = true + result["hasArgoAppAccess"] = true + result["hasFluxAppAccess"] = true + continue } + + roleFragments := strings.Split(item, "_") + resourceActionFragment := strings.Split(roleFragments[0], ":") + + if len(resourceActionFragment) < 2 { + continue + } + + if resourceActionFragment[0] == "argo-app" && (resourceActionFragment[1] == "admin" || resourceActionFragment[1] == "view") { + result["hasArgoAppAccess"] = true + } + + if resourceActionFragment[0] == "flux-app" && (resourceActionFragment[1] == "admin" || resourceActionFragment[1] == "view") { + result["hasFluxAppAccess"] = true + } + } common.WriteJsonResp(w, err, result, http.StatusOK) } diff --git a/api/fluxApplication/FluxApplicationRestHandler.go b/api/fluxApplication/FluxApplicationRestHandler.go index f1ef3d616c..bc1b9c3c02 100644 --- a/api/fluxApplication/FluxApplicationRestHandler.go +++ b/api/fluxApplication/FluxApplicationRestHandler.go @@ -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" @@ -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 @@ -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) { @@ -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 { diff --git a/api/helm-app/wire_helmApp.go b/api/helm-app/wire_helmApp.go index 86f45e672c..af668990bb 100644 --- a/api/helm-app/wire_helmApp.go +++ b/api/helm-app/wire_helmApp.go @@ -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)), ) diff --git a/api/k8s/application/k8sApplicationRestHandler.go b/api/k8s/application/k8sApplicationRestHandler.go index a3d64a7ff5..2ca6dd9128 100644 --- a/api/k8s/application/k8sApplicationRestHandler.go +++ b/api/k8s/application/k8sApplicationRestHandler.go @@ -91,6 +91,7 @@ type K8sApplicationRestHandlerImpl struct { validator *validator.Validate enforcerUtil rbac.EnforcerUtil enforcerUtilHelm rbac.EnforcerUtilHelm + enforcerUtilGitOps rbac.EnforcerUtilGitOps helmAppService client.HelmAppService userService user.UserService k8sCommonService k8s.K8sCommonService @@ -99,7 +100,7 @@ type K8sApplicationRestHandlerImpl struct { argoApplicationReadService read.ArgoApplicationReadService } -func NewK8sApplicationRestHandlerImpl(logger *zap.SugaredLogger, k8sApplicationService application2.K8sApplicationService, pump connector.Pump, terminalSessionHandler terminal.TerminalSessionHandler, enforcer casbin.Enforcer, enforcerUtilHelm rbac.EnforcerUtilHelm, enforcerUtil rbac.EnforcerUtil, helmAppService client.HelmAppService, userService user.UserService, k8sCommonService k8s.K8sCommonService, validator *validator.Validate, envVariables *util.EnvironmentVariables, fluxAppService fluxApplication.FluxApplicationService, argoApplicationReadService read.ArgoApplicationReadService, +func NewK8sApplicationRestHandlerImpl(logger *zap.SugaredLogger, k8sApplicationService application2.K8sApplicationService, pump connector.Pump, terminalSessionHandler terminal.TerminalSessionHandler, enforcer casbin.Enforcer, enforcerUtilHelm rbac.EnforcerUtilHelm, enforcerUtilGitOps rbac.EnforcerUtilGitOps, enforcerUtil rbac.EnforcerUtil, helmAppService client.HelmAppService, userService user.UserService, k8sCommonService k8s.K8sCommonService, validator *validator.Validate, envVariables *util.EnvironmentVariables, fluxAppService fluxApplication.FluxApplicationService, argoApplicationReadService read.ArgoApplicationReadService, ) *K8sApplicationRestHandlerImpl { return &K8sApplicationRestHandlerImpl{ logger: logger, @@ -109,6 +110,7 @@ func NewK8sApplicationRestHandlerImpl(logger *zap.SugaredLogger, k8sApplicationS enforcer: enforcer, validator: validator, enforcerUtilHelm: enforcerUtilHelm, + enforcerUtilGitOps: enforcerUtilGitOps, enforcerUtil: enforcerUtil, helmAppService: helmAppService, userService: userService, @@ -207,7 +209,20 @@ func (handler *K8sApplicationRestHandlerImpl) GetResource(w http.ResponseWriter, canUpdate := false // Obfuscate secret if user does not have edit access - if request.AppIdentifier == nil && request.DevtronAppIdentifier == nil && request.AppType != bean2.ArgoAppType && request.ClusterId > 0 { // if the appType is not argoAppType,then verify logic w.r.t resource browser, when rbac for argoApp is introduced, handle rbac accordingly + if request.AppType == bean2.ArgoAppType && request.ExternalArgoAppIdentifier != nil { + // External Argo app: edit access is decided by the app-level permission, not by the + // resource-browser cluster entity. Without this an Argo admin would never see Secret + // values, because canUpdate would stay false and the manifest would be masked below. + rbacObject := handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(request.ExternalArgoAppIdentifier.ClusterId, + request.ExternalArgoAppIdentifier.Namespace, request.ExternalArgoAppIdentifier.AppName) + canUpdate = len(rbacObject) > 0 && + handler.enforcer.Enforce(token, casbin.ResourceArgoApp, casbin.ActionUpdate, rbacObject) + } else if request.AppType == bean2.FluxAppType && request.ExternalFluxAppIdentifier != nil { + rbacObject := handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(request.ExternalFluxAppIdentifier.ClusterId, + request.ExternalFluxAppIdentifier.Namespace, request.ExternalFluxAppIdentifier.Name) + canUpdate = len(rbacObject) > 0 && + handler.enforcer.Enforce(token, casbin.ResourceFluxApp, casbin.ActionUpdate, rbacObject) + } else if request.AppIdentifier == nil && request.DevtronAppIdentifier == nil && request.AppType != bean2.ArgoAppType && request.ClusterId > 0 { // if the appType is not argoAppType,then verify logic w.r.t resource browser // Verify update access for Resource Browser canUpdate = handler.k8sApplicationService.ValidateClusterResourceBean(r.Context(), request.ClusterId, resource.ManifestResponse.Manifest, request.K8sRequest.ResourceIdentifier.GroupVersionKind, handler.getRbacCallbackForResource(token, casbin.ActionUpdate)) if !canUpdate { @@ -299,7 +314,8 @@ func (handler *K8sApplicationRestHandlerImpl) GetHostUrlsByBatch(w http.Response return } // RBAC enforcer applying - if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); !ok { + rbacObject := handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(appIdentifier.ClusterId, appIdentifier.Namespace, appIdentifier.AppName) + if len(rbacObject) == 0 || !handler.enforcer.Enforce(token, casbin.ResourceArgoApp, casbin.ActionGet, rbacObject) { common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) return } @@ -326,7 +342,8 @@ func (handler *K8sApplicationRestHandlerImpl) GetHostUrlsByBatch(w http.Response return } // RBAC enforcer applying - if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); !ok { + rbacObject := handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(appIdentifier.ClusterId, appIdentifier.Namespace, appIdentifier.Name) + if len(rbacObject) == 0 || !handler.enforcer.Enforce(token, casbin.ResourceFluxApp, casbin.ActionGet, rbacObject) { common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) return } @@ -718,7 +735,9 @@ func (handler *K8sApplicationRestHandlerImpl) requestValidationAndRBAC(w http.Re return } //RBAC enforcer starts here - if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); !ok { + rbacObject := handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(request.ExternalFluxAppIdentifier.ClusterId, + request.ExternalFluxAppIdentifier.Namespace, request.ExternalFluxAppIdentifier.Name) + if len(rbacObject) == 0 || !handler.enforcer.Enforce(token, casbin.ResourceFluxApp, casbin.ActionGet, rbacObject) { common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) return } @@ -738,7 +757,8 @@ func (handler *K8sApplicationRestHandlerImpl) requestValidationAndRBAC(w http.Re } //RBAC enforcer starts here - if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); !ok { + rbacObject := handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(appIdentifier.ClusterId, appIdentifier.Namespace, appIdentifier.AppName) + if len(rbacObject) == 0 || !handler.enforcer.Enforce(token, casbin.ResourceArgoApp, casbin.ActionGet, rbacObject) { common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) return } @@ -803,7 +823,9 @@ func (handler *K8sApplicationRestHandlerImpl) GetTerminalSession(w http.Response //RBAC enforcer Ends } else if resourceRequestBean.ExternalFluxAppIdentifier != nil { // RBAC enforcer applying For external flux app - if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionUpdate, "*"); !ok { + rbacObject := handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(resourceRequestBean.ExternalFluxAppIdentifier.ClusterId, + resourceRequestBean.ExternalFluxAppIdentifier.Namespace, resourceRequestBean.ExternalFluxAppIdentifier.Name) + if len(rbacObject) == 0 || !handler.enforcer.Enforce(token, casbin.ResourceFluxApp, casbin.ActionUpdate, rbacObject) { common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) return } @@ -811,7 +833,13 @@ func (handler *K8sApplicationRestHandlerImpl) GetTerminalSession(w http.Response } else if resourceRequestBean.ExternalArgoApplicationName != "" { // RBAC enforcer applying For external Argo app - if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionUpdate, "*"); !ok { + if request.ExternalArgoAppIdentifier == nil { + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + return + } + rbacObject := handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(request.ExternalArgoAppIdentifier.ClusterId, + request.ExternalArgoAppIdentifier.Namespace, request.ExternalArgoAppIdentifier.AppName) + if len(rbacObject) == 0 || !handler.enforcer.Enforce(token, casbin.ResourceArgoApp, casbin.ActionUpdate, rbacObject) { common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) return } @@ -1100,7 +1128,10 @@ func (handler *K8sApplicationRestHandlerImpl) DeleteEphemeralContainer(w http.Re func (handler *K8sApplicationRestHandlerImpl) handleEphemeralRBAC(podName, namespace string, w http.ResponseWriter, r *http.Request) *bean3.ResourceRequestBean { token := r.Header.Get("token") - _, resourceRequestBean, err := handler.k8sApplicationService.ValidateTerminalRequestQuery(r) + // terminalRequest is needed for the external Argo identifier: ValidateTerminalRequestQuery + // puts the full ArgoAppIdentifier on the request, while resourceRequestBean receives only + // the app name and clusterId. + terminalRequest, resourceRequestBean, err := handler.k8sApplicationService.ValidateTerminalRequestQuery(r) if err != nil { common.WriteJsonResp(w, err, nil, http.StatusBadRequest) return resourceRequestBean @@ -1125,14 +1156,22 @@ func (handler *K8sApplicationRestHandlerImpl) handleEphemeralRBAC(podName, names //RBAC enforcer Ends } else if resourceRequestBean.ExternalFluxAppIdentifier != nil { //RBAC enforcer starts here - if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); !ok { + rbacObject := handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(resourceRequestBean.ExternalFluxAppIdentifier.ClusterId, + resourceRequestBean.ExternalFluxAppIdentifier.Namespace, resourceRequestBean.ExternalFluxAppIdentifier.Name) + if len(rbacObject) == 0 || !handler.enforcer.Enforce(token, casbin.ResourceFluxApp, casbin.ActionUpdate, rbacObject) { common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) return resourceRequestBean } //RBAC enforcer ends here } else if resourceRequestBean.ExternalArgoApplicationName != "" { //RBAC enforcer starts here - if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); !ok { + if terminalRequest == nil || terminalRequest.ExternalArgoAppIdentifier == nil { + common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) + return resourceRequestBean + } + rbacObject := handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(terminalRequest.ExternalArgoAppIdentifier.ClusterId, + terminalRequest.ExternalArgoAppIdentifier.Namespace, terminalRequest.ExternalArgoAppIdentifier.AppName) + if len(rbacObject) == 0 || !handler.enforcer.Enforce(token, casbin.ResourceArgoApp, casbin.ActionUpdate, rbacObject) { common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) return resourceRequestBean } @@ -1179,7 +1218,11 @@ func (handler *K8sApplicationRestHandlerImpl) verifyRbacForAppRequests(token str return false, err } //RBAC enforcer starts here - if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, actionType, "*"); !ok { + rbacObject = handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(argoAppIdentifier.ClusterId, argoAppIdentifier.Namespace, argoAppIdentifier.AppName) + if len(rbacObject) == 0 { + return false, nil + } + if ok := handler.enforcer.Enforce(token, casbin.ResourceArgoApp, actionType, rbacObject); !ok { return false, nil } return true, nil @@ -1247,7 +1290,11 @@ func (handler *K8sApplicationRestHandlerImpl) verifyRbacForAppRequests(token str return false, err } //RBAC enforcer starts here - if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, actionType, "*"); !ok { + rbacObject = handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(appIdentifier.ClusterId, appIdentifier.Namespace, appIdentifier.Name) + if len(rbacObject) == 0 { + return false, nil + } + if ok := handler.enforcer.Enforce(token, casbin.ResourceFluxApp, actionType, rbacObject); !ok { return false, nil } return true, nil diff --git a/api/userResource/bean/bean.go b/api/userResource/bean/bean.go index b6f5c9ea2f..375b6de21a 100644 --- a/api/userResource/bean/bean.go +++ b/api/userResource/bean/bean.go @@ -31,6 +31,8 @@ type AppAndJobReqDto struct { } type ClusterReqDto struct { *bean3.ResourceRequestBean + ClusterIds []int `json:"clusterIds,omitempty"` + EnvironmentIdentifiers []string `json:"environmentIdentifiers,omitempty"` } type JobWorkflowReqDto struct { *bean2.WorkflowNamesRequest diff --git a/argo-rbac-test/Makefile b/argo-rbac-test/Makefile new file mode 100644 index 0000000000..947ef7c1cc --- /dev/null +++ b/argo-rbac-test/Makefile @@ -0,0 +1,215 @@ +# Test fixtures for Argo CD / Flux CD app-level RBAC. +# +# Creates 4 Argo Applications and 4 Flux Kustomizations across 2 namespaces, +# plus a deliberate Kustomization/HelmRelease name collision. +# +# The multi-cluster dimension comes from the kube context, not the manifests: +# apply the same set to two contexts and register both clusters in Devtron. +# +# make apply CONTEXT=k3d-dev-1 +# make apply CONTEXT=k3d-dev-2 +# make status CONTEXT=k3d-dev-1 +# make clean CONTEXT=k3d-dev-1 +# +# CONTEXT defaults to the current context. NS1/NS2 are informational — the +# namespaces are hardcoded in the manifests; change them there if needed. + +CONTEXT ?= $(shell kubectl config current-context) +KUBECTL = kubectl --context=$(CONTEXT) +NS1 ?= devtroncd-ent-3 +NS2 ?= devtroncd-oss-2 + +# --------------------------------------------------------------------------- +# Migrations +# +# Two databases, two migration trees, both must be applied: +# scripts/sql -> orchestrator DB (roles, rbac_role_data, rbac_policy_data, ...) +# scripts/casbin -> casbin DB (casbin_rule: the super-admin p-lines) +# +# Connection settings default to this dev setup and are overridden by ../.env +# if present, or by CLI args. Nothing needs passing for the normal case. +# The template cache loads once at boot, so RESTART the orchestrator after +# migrating or new rbac_role_data rows will not be picked up. +# --------------------------------------------------------------------------- +-include ../.env + +PG_ADDR ?= localhost +PG_PORT ?= 5432 +PG_USER ?= postgres +PG_DATABASE ?= orchestrator_oss_2 +CASBIN_DATABASE ?= casbin_oss_2 +STEPS ?= 1 + +SQL_DIR := ../scripts/sql +CASBIN_DIR := ../scripts/casbin +ORCH_DSN = postgres://$(PG_USER):$(PG_PASSWORD)@$(PG_ADDR):$(PG_PORT)/$(PG_DATABASE)?sslmode=disable +CASBIN_DSN = postgres://$(PG_USER):$(PG_PASSWORD)@$(PG_ADDR):$(PG_PORT)/$(CASBIN_DATABASE)?sslmode=disable + +.PHONY: help apply apply-argo apply-flux namespaces clean clean-argo clean-flux \ + clean-namespaces status check-prereqs argo-crds argo-any-namespace \ + migrate-check migrate-info migrate-up migrate-up-orchestrator migrate-up-casbin \ + migrate-down migrate-down-orchestrator migrate-down-casbin migrate-force + +help: + @echo "Targets (all accept CONTEXT=):" + @echo " check-prereqs verify namespaces and CRDs are present" + @echo " namespaces create the workload namespaces" + @echo " argo-crds install only the Argo Application CRD (list-only testing)" + @echo " argo-any-namespace allow Argo to reconcile apps in NS1/NS2" + @echo " apply apply all Argo + Flux fixtures" + @echo " apply-argo apply the 4 Argo Applications" + @echo " apply-flux apply Flux sources, Kustomizations, collision HelmRelease" + @echo " status list what Devtron will see" + @echo " clean delete everything created here" + @echo "" + @echo "Migrations (override PG_* / CASBIN_DATABASE on the CLI):" + @echo " migrate-info show target databases and current versions" + @echo " migrate-up apply both trees (orchestrator + casbin)" + @echo " migrate-down roll back STEPS=1 on both trees" + @echo " migrate-force clear a dirty state: DB=orchestrator|casbin VERSION=n" + +check-prereqs: + @echo "== context: $(CONTEXT)" + @for ns in $(NS1) $(NS2); do \ + $(KUBECTL) get ns $$ns >/dev/null 2>&1 \ + && echo " ns $$ns: ok" \ + || echo " ns $$ns: MISSING - create it or edit the manifests"; \ + done + @$(KUBECTL) get crd applications.argoproj.io >/dev/null 2>&1 \ + && echo " Argo Application CRD: ok" \ + || echo " Argo Application CRD: MISSING - run 'make argo-crds' or install Argo CD" + @$(KUBECTL) get crd kustomizations.kustomize.toolkit.fluxcd.io >/dev/null 2>&1 \ + && echo " Flux Kustomization CRD: ok" \ + || echo " Flux Kustomization CRD: MISSING - run 'flux install'" + @$(KUBECTL) get crd helmreleases.helm.toolkit.fluxcd.io >/dev/null 2>&1 \ + && echo " Flux HelmRelease CRD: ok" \ + || echo " Flux HelmRelease CRD: MISSING - run 'flux install'" + +# Devtron reads Application CRs through the k8s API and never contacts argocd-server, +# so the listing works with the CRD alone. Detail/manifest/terminal will NOT work, +# because health, sync status and the managed-resource list are parsed out of +# .status, which only a running controller populates. +argo-crds: + $(KUBECTL) apply -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/crds/application-crd.yaml + +# Without this, Argo's controller ignores Applications outside its own namespace, +# so they list with blank status and an empty resource tree. +argo-any-namespace: + $(KUBECTL) -n argocd patch cm argocd-cmd-params-cm --type merge \ + -p '{"data":{"application.namespaces":"$(NS1),$(NS2)"}}' + $(KUBECTL) -n argocd rollout restart deploy/argocd-server deploy/argocd-application-controller + +apply: namespaces apply-argo apply-flux + +# Workload namespaces. Argo creates its own via CreateNamespace=true, but Flux +# does not create targetNamespace, so these must exist first. +namespaces: + $(KUBECTL) apply -f namespaces.yaml + +apply-argo: namespaces + $(KUBECTL) apply -f argo/ + +# Sources first — a Kustomization whose sourceRef is missing stays in a failed +# state (it still appears in the listing, but with no resource tree). +apply-flux: namespaces + $(KUBECTL) apply -f flux/gitrepo-ns1.yaml -f flux/gitrepo-ns2.yaml -f flux/helmrepo-ns1.yaml + $(KUBECTL) apply -f flux/app1ns1.yaml -f flux/app1ns2.yaml -f flux/app2ns1.yaml -f flux/app2ns2.yaml + $(KUBECTL) apply -f flux/collision-helmrelease-ns1.yaml + +status: + @echo "== Argo Applications (name/namespace = the RBAC object) ==" + @$(KUBECTL) get applications.argoproj.io -A \ + -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,SYNC:.status.sync.status,HEALTH:.status.health.status 2>/dev/null \ + || echo " none / CRD missing" + @echo + @echo "== Flux Kustomizations ==" + @$(KUBECTL) get kustomizations -A 2>/dev/null || echo " none / CRD missing" + @echo + @echo "== Flux HelmReleases (collision fixture) ==" + @$(KUBECTL) get helmreleases -A 2>/dev/null || echo " none / CRD missing" + +# Order matters: delete the CRs first so the controllers stop reconciling, then +# drop the workload namespaces. Deleting namespaces first would leave Argo and +# Flux fighting to recreate resources. +clean: clean-argo clean-flux clean-namespaces + +# The Applications carry no finalizer, so deleting the CR does not cascade-delete +# what the controller deployed — clean-namespaces handles that. +clean-argo: + -$(KUBECTL) delete -f argo/ --ignore-not-found + +clean-flux: + -$(KUBECTL) delete -f flux/collision-helmrelease-ns1.yaml --ignore-not-found + -$(KUBECTL) delete -f flux/app1ns1.yaml -f flux/app1ns2.yaml -f flux/app2ns1.yaml -f flux/app2ns2.yaml --ignore-not-found + -$(KUBECTL) delete -f flux/gitrepo-ns1.yaml -f flux/gitrepo-ns2.yaml -f flux/helmrepo-ns1.yaml --ignore-not-found + +clean-namespaces: + -$(KUBECTL) delete ns -l rbac-test=true --ignore-not-found + +# --------------------------------------------------------------------------- +# Migration targets +# --------------------------------------------------------------------------- + +migrate-check: + @command -v migrate >/dev/null 2>&1 || { \ + echo "golang-migrate not found. Install it:"; \ + echo " brew install golang-migrate"; exit 1; } + +migrate-info: migrate-check + @echo "orchestrator : $(PG_USER)@$(PG_ADDR):$(PG_PORT)/$(PG_DATABASE) <- $(SQL_DIR)" + @echo "casbin : $(PG_USER)@$(PG_ADDR):$(PG_PORT)/$(CASBIN_DATABASE) <- $(CASBIN_DIR)" + @if [ "$(CASBIN_DATABASE)" = "casbin" ]; then \ + echo ""; \ + echo "WARNING: CASBIN_DATABASE is the default 'casbin'. On a shared Postgres that"; \ + echo " database is shared with every other developer's orchestrator."; \ + echo " Set CASBIN_DATABASE=casbin_ in ../.env before migrating."; \ + fi + @echo "" + @echo "-- current versions --" + @printf "orchestrator : "; migrate -path $(SQL_DIR) -database "$(ORCH_DSN)" version 2>&1 || true + @printf "casbin : "; migrate -path $(CASBIN_DIR) -database "$(CASBIN_DSN)" version 2>&1 || true + +migrate-up: migrate-up-orchestrator migrate-up-casbin + @echo "" + @echo "Both trees applied. Restart the orchestrator — rbac_role_data and" + @echo "rbac_policy_data are cached in memory at boot (UserCommonService.go:102)." + +migrate-up-orchestrator: migrate-check + @echo "== orchestrator ($(PG_DATABASE)) ==" + @migrate -path $(SQL_DIR) -database "$(ORCH_DSN)" up + +# Forgetting this one is the classic failure: super-admins silently lose access +# to the new resources, because there is no catch-all row in casbin_rule. +migrate-up-casbin: migrate-check + @echo "== casbin ($(CASBIN_DATABASE)) ==" + @migrate -path $(CASBIN_DIR) -database "$(CASBIN_DSN)" up + +# STEPS defaults to 1. Bare `migrate down` rolls back EVERYTHING, so it is never +# used here. +migrate-down: migrate-down-orchestrator migrate-down-casbin + +migrate-down-orchestrator: migrate-check + @echo "== orchestrator ($(PG_DATABASE)) down $(STEPS) ==" + @migrate -path $(SQL_DIR) -database "$(ORCH_DSN)" down $(STEPS) + +# Guarded: rolling back the shared 'casbin' database would strip policies from +# every orchestrator pointed at this Postgres. Override with I_KNOW=1. +migrate-down-casbin: migrate-check + @if [ "$(CASBIN_DATABASE)" = "casbin" ] && [ "$(I_KNOW)" != "1" ]; then \ + echo "REFUSING: CASBIN_DATABASE=casbin is the shared database."; \ + echo " Use your own (CASBIN_DATABASE=casbin_oss_2), or pass I_KNOW=1."; \ + exit 1; \ + fi + @echo "== casbin ($(CASBIN_DATABASE)) down $(STEPS) ==" + @migrate -path $(CASBIN_DIR) -database "$(CASBIN_DSN)" down $(STEPS) + +# A migration that fails partway leaves the version marked dirty and blocks all +# further migrations. Clear it by forcing to the last known-good version: +# make migrate-force DB=orchestrator VERSION=36204600 +migrate-force: migrate-check + @test -n "$(VERSION)" || { echo "VERSION= required"; exit 1; } + @case "$(DB)" in \ + orchestrator) migrate -path $(SQL_DIR) -database "$(ORCH_DSN)" force $(VERSION) ;; \ + casbin) migrate -path $(CASBIN_DIR) -database "$(CASBIN_DSN)" force $(VERSION) ;; \ + *) echo "DB=orchestrator|casbin required"; exit 1 ;; \ + esac diff --git a/argo-rbac-test/argo/app1ns1.yaml b/argo-rbac-test/argo/app1ns1.yaml new file mode 100644 index 0000000000..b20d245fed --- /dev/null +++ b/argo-rbac-test/argo/app1ns1.yaml @@ -0,0 +1,18 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: app-1 + namespace: devtroncd-ent-3 # <- this is what lands in the RBAC object +spec: + project: default + source: + repoURL: https://github.com/argoproj/argocd-example-apps + path: guestbook + targetRevision: HEAD + destination: + server: https://kubernetes.default.svc # special-cased to "same cluster as the CR" + namespace: wl-argo-1 # own namespace, so resource ownership is testable + syncPolicy: + automated: { prune: true, selfHeal: true } + syncOptions: + - CreateNamespace=true diff --git a/argo-rbac-test/argo/app1ns2.yaml b/argo-rbac-test/argo/app1ns2.yaml new file mode 100644 index 0000000000..9d302ef968 --- /dev/null +++ b/argo-rbac-test/argo/app1ns2.yaml @@ -0,0 +1,18 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: app-1 + namespace: devtroncd-oss-2 # <- this is what lands in the RBAC object +spec: + project: default + source: + repoURL: https://github.com/argoproj/argocd-example-apps + path: guestbook + targetRevision: HEAD + destination: + server: https://kubernetes.default.svc # special-cased to "same cluster as the CR" + namespace: wl-argo-3 # own namespace, so resource ownership is testable + syncPolicy: + automated: { prune: true, selfHeal: true } + syncOptions: + - CreateNamespace=true diff --git a/argo-rbac-test/argo/app2ns1.yaml b/argo-rbac-test/argo/app2ns1.yaml new file mode 100644 index 0000000000..e7aa6d07d5 --- /dev/null +++ b/argo-rbac-test/argo/app2ns1.yaml @@ -0,0 +1,18 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: app-2 + namespace: devtroncd-ent-3 # <- this is what lands in the RBAC object +spec: + project: default + source: + repoURL: https://github.com/argoproj/argocd-example-apps + path: guestbook + targetRevision: HEAD + destination: + server: https://kubernetes.default.svc # special-cased to "same cluster as the CR" + namespace: wl-argo-2 # own namespace, so resource ownership is testable + syncPolicy: + automated: { prune: true, selfHeal: true } + syncOptions: + - CreateNamespace=true diff --git a/argo-rbac-test/argo/app2ns2.yaml b/argo-rbac-test/argo/app2ns2.yaml new file mode 100644 index 0000000000..562cea49e2 --- /dev/null +++ b/argo-rbac-test/argo/app2ns2.yaml @@ -0,0 +1,18 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: app-2 + namespace: devtroncd-oss-2 # <- this is what lands in the RBAC object +spec: + project: default + source: + repoURL: https://github.com/argoproj/argocd-example-apps + path: guestbook + targetRevision: HEAD + destination: + server: https://kubernetes.default.svc # special-cased to "same cluster as the CR" + namespace: wl-argo-4 # own namespace, so resource ownership is testable + syncPolicy: + automated: { prune: true, selfHeal: true } + syncOptions: + - CreateNamespace=true diff --git a/argo-rbac-test/flux/app1ns1.yaml b/argo-rbac-test/flux/app1ns1.yaml new file mode 100644 index 0000000000..052d2a108f --- /dev/null +++ b/argo-rbac-test/flux/app1ns1.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: app-1 + namespace: devtroncd-ent-3 # <- this is what lands in the RBAC object +spec: + interval: 10m + prune: true + targetNamespace: wl-flux-1 # without this, podinfo deploys INTO devtroncd-ent-3 + sourceRef: + kind: GitRepository + name: podinfo + path: ./kustomize diff --git a/argo-rbac-test/flux/app1ns2.yaml b/argo-rbac-test/flux/app1ns2.yaml new file mode 100644 index 0000000000..f7a8112f9e --- /dev/null +++ b/argo-rbac-test/flux/app1ns2.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: app-1 + namespace: devtroncd-oss-2 # <- this is what lands in the RBAC object +spec: + interval: 10m + prune: true + targetNamespace: wl-flux-3 # without this, podinfo deploys INTO devtroncd-oss-2 + sourceRef: + kind: GitRepository + name: podinfo + path: ./kustomize diff --git a/argo-rbac-test/flux/app2ns1.yaml b/argo-rbac-test/flux/app2ns1.yaml new file mode 100644 index 0000000000..cc4059fcce --- /dev/null +++ b/argo-rbac-test/flux/app2ns1.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: app-2 + namespace: devtroncd-ent-3 # <- this is what lands in the RBAC object +spec: + interval: 10m + prune: true + targetNamespace: wl-flux-2 # without this, podinfo deploys INTO devtroncd-ent-3 + sourceRef: + kind: GitRepository + name: podinfo + path: ./kustomize diff --git a/argo-rbac-test/flux/app2ns2.yaml b/argo-rbac-test/flux/app2ns2.yaml new file mode 100644 index 0000000000..dfc48669f5 --- /dev/null +++ b/argo-rbac-test/flux/app2ns2.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: app-2 + namespace: devtroncd-oss-2 # <- this is what lands in the RBAC object +spec: + interval: 10m + prune: true + targetNamespace: wl-flux-4 # without this, podinfo deploys INTO devtroncd-oss-2 + sourceRef: + kind: GitRepository + name: podinfo + path: ./kustomize diff --git a/argo-rbac-test/flux/collision-helmrelease-ns1.yaml b/argo-rbac-test/flux/collision-helmrelease-ns1.yaml new file mode 100644 index 0000000000..99c70acbef --- /dev/null +++ b/argo-rbac-test/flux/collision-helmrelease-ns1.yaml @@ -0,0 +1,30 @@ +# Deliberate name collision fixture. +# +# This HelmRelease shares its name AND namespace with the Kustomization in +# flux/app1ns1.yaml (app-1 / devtroncd-ent-3). +# +# Because the RBAC object is __/ with no resource-type +# segment, both objects collapse to the SAME permission: +# devtroncd-ent-3__/app-1 <- wait, order is __/ +# __devtroncd-ent-3/app-1 +# +# Expected behaviour: granting on app-1 grants BOTH the Kustomization and the +# HelmRelease. Verify that is acceptable — it is the accepted trade-off of dropping +# the type segment, and it cannot be changed later without an arity change. +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: app-1 + namespace: devtroncd-ent-3 +spec: + interval: 10m + targetNamespace: wl-flux-5 # without this it installs INTO devtroncd-ent-3 + install: + createNamespace: false # namespaces.yaml owns it, so cleanup is predictable + chart: + spec: + chart: podinfo + version: '6.*' + sourceRef: + kind: HelmRepository + name: podinfo diff --git a/argo-rbac-test/flux/gitrepo-ns1.yaml b/argo-rbac-test/flux/gitrepo-ns1.yaml new file mode 100644 index 0000000000..1c1ce20eb9 --- /dev/null +++ b/argo-rbac-test/flux/gitrepo-ns1.yaml @@ -0,0 +1,10 @@ +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: podinfo + namespace: devtroncd-ent-3 +spec: + interval: 10m + url: https://github.com/stefanprodan/podinfo + ref: + branch: master diff --git a/argo-rbac-test/flux/gitrepo-ns2.yaml b/argo-rbac-test/flux/gitrepo-ns2.yaml new file mode 100644 index 0000000000..5eeb78cf24 --- /dev/null +++ b/argo-rbac-test/flux/gitrepo-ns2.yaml @@ -0,0 +1,10 @@ +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: podinfo + namespace: devtroncd-oss-2 +spec: + interval: 10m + url: https://github.com/stefanprodan/podinfo + ref: + branch: master diff --git a/argo-rbac-test/flux/helmrepo-ns1.yaml b/argo-rbac-test/flux/helmrepo-ns1.yaml new file mode 100644 index 0000000000..141553456e --- /dev/null +++ b/argo-rbac-test/flux/helmrepo-ns1.yaml @@ -0,0 +1,8 @@ +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: podinfo + namespace: devtroncd-ent-3 +spec: + interval: 10m + url: https://stefanprodan.github.io/podinfo diff --git a/argo-rbac-test/namespaces.yaml b/argo-rbac-test/namespaces.yaml new file mode 100644 index 0000000000..2b794dfdbf --- /dev/null +++ b/argo-rbac-test/namespaces.yaml @@ -0,0 +1,47 @@ +# Workload namespaces for the fixtures. +# +# These are where the apps DEPLOY TO. They are not part of the RBAC object — +# that uses the namespace the Application / Kustomization CR itself lives in +# (devtroncd-ent-3, devtroncd-oss-2). +# +# Each app gets its own workload namespace so that "access to app A must not +# grant access to app B's pods" is actually testable. Sharing one namespace +# would make every resource legitimately belong to every app's resource tree. +# +# Argo creates its own destinations via CreateNamespace=true, but Flux does not +# create targetNamespace, so they are declared here for both. +apiVersion: v1 +kind: Namespace +metadata: { name: wl-argo-1, labels: { rbac-test: "true" } } +--- +apiVersion: v1 +kind: Namespace +metadata: { name: wl-argo-2, labels: { rbac-test: "true" } } +--- +apiVersion: v1 +kind: Namespace +metadata: { name: wl-argo-3, labels: { rbac-test: "true" } } +--- +apiVersion: v1 +kind: Namespace +metadata: { name: wl-argo-4, labels: { rbac-test: "true" } } +--- +apiVersion: v1 +kind: Namespace +metadata: { name: wl-flux-1, labels: { rbac-test: "true" } } +--- +apiVersion: v1 +kind: Namespace +metadata: { name: wl-flux-2, labels: { rbac-test: "true" } } +--- +apiVersion: v1 +kind: Namespace +metadata: { name: wl-flux-3, labels: { rbac-test: "true" } } +--- +apiVersion: v1 +kind: Namespace +metadata: { name: wl-flux-4, labels: { rbac-test: "true" } } +--- +apiVersion: v1 +kind: Namespace +metadata: { name: wl-flux-5, labels: { rbac-test: "true" } } diff --git a/pkg/auth/authorisation/casbin/rbacpolicy.go b/pkg/auth/authorisation/casbin/rbacpolicy.go index d61234b868..ddc0a452bc 100644 --- a/pkg/auth/authorisation/casbin/rbacpolicy.go +++ b/pkg/auth/authorisation/casbin/rbacpolicy.go @@ -45,6 +45,12 @@ const ( ResourceAdmin = "admin" ResourceGlobal = "global-resource" ResourceHelmApp = "helm-app" + + // ResourceArgoApp, ResourceFluxApp are used for app-level RBAC on external + // Argo CD / Flux CD applications. Object shape is __/. + ResourceArgoApp = "argo-app" + ResourceFluxApp = "flux-app" + ActionGet = "get" ActionCreate = "create" ActionUpdate = "update" diff --git a/pkg/auth/user/bean/bean.go b/pkg/auth/user/bean/bean.go index 2b03b0ad49..5447775239 100644 --- a/pkg/auth/user/bean/bean.go +++ b/pkg/auth/user/bean/bean.go @@ -66,6 +66,8 @@ const ( const ( DEVTRON_APP = "devtron-app" APP_ACCESS_TYPE_HELM = "helm-app" + APP_ACCESS_TYPE_ARGO = "argo-app" + APP_ACCESS_TYPE_FLUX = "flux-app" EmptyAccessType = "" ) diff --git a/pkg/auth/user/repository/UserAuthRepository.go b/pkg/auth/user/repository/UserAuthRepository.go index ce8fe2940f..76b797ff20 100644 --- a/pkg/auth/user/repository/UserAuthRepository.go +++ b/pkg/auth/user/repository/UserAuthRepository.go @@ -22,11 +22,12 @@ package repository import ( "encoding/json" "fmt" + "strings" + "time" + bean3 "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin/bean" "github.com/devtron-labs/devtron/pkg/auth/user/adapter" bean4 "github.com/devtron-labs/devtron/pkg/auth/user/repository/bean" - "strings" - "time" "github.com/devtron-labs/devtron/api/bean" casbin2 "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin" @@ -1135,6 +1136,46 @@ func (impl UserAuthRepositoryImpl) GetRoleForOtherEntity(team, app, env, act, ac queryParams = append(queryParams, accessType) } + _, err = impl.dbConnection.Query(&model, query, queryParams...) + } else if team == "" && len(app) > 0 && len(env) > 0 && len(act) > 0 { + var queryParams []interface{} + //this is applicable for entities that have no project, e.g. external argo/flux apps, + //where the scope is environment(cluster__namespace) + app + query := "SELECT role.* FROM roles role WHERE coalesce(role.team,'') = ? AND role.entity_name=? AND role.environment=? AND role.action=?" + queryParams = append(queryParams, EMPTY_PLACEHOLDER_FOR_QUERY, app, env, act) + if oldValues { + query = query + " and role.access_type is NULL" + } else { + query += " and role.access_type = ? " + queryParams = append(queryParams, accessType) + } + + _, err = impl.dbConnection.Query(&model, query, queryParams...) + } else if team == "" && app == "" && len(env) > 0 && len(act) > 0 { + var queryParams []interface{} + //no project, all apps of an environment(cluster__namespace) + query := "SELECT role.* FROM roles role WHERE coalesce(role.team,'') = ? AND coalesce(role.entity_name,'')=? AND role.environment=? AND role.action=?" + queryParams = append(queryParams, EMPTY_PLACEHOLDER_FOR_QUERY, EMPTY_PLACEHOLDER_FOR_QUERY, env, act) + if oldValues { + query = query + " and role.access_type is NULL" + } else { + query += " and role.access_type = ? " + queryParams = append(queryParams, accessType) + } + + _, err = impl.dbConnection.Query(&model, query, queryParams...) + } else if team == "" && len(app) > 0 && env == "" && len(act) > 0 { + var queryParams []interface{} + //no project, an app across all environments + query := "SELECT role.* FROM roles role WHERE coalesce(role.team,'') = ? AND role.entity_name=? AND coalesce(role.environment,'')=? AND role.action=?" + queryParams = append(queryParams, EMPTY_PLACEHOLDER_FOR_QUERY, app, EMPTY_PLACEHOLDER_FOR_QUERY, act) + if oldValues { + query = query + " and role.access_type is NULL" + } else { + query += " and role.access_type = ? " + queryParams = append(queryParams, accessType) + } + _, err = impl.dbConnection.Query(&model, query, queryParams...) } else if team == "" && app == "" && env == "" && len(act) > 0 { var queryParams []interface{} @@ -1152,6 +1193,8 @@ func (impl UserAuthRepositoryImpl) GetRoleForOtherEntity(team, app, env, act, ac } else if team == "" && app == "" && env == "" && act == "" { return model, nil } else { + impl.Logger.Warnw("no query branch for the given role filter combination, returning empty role", + "team", team, "app", app, "env", env, "action", act, "accessType", accessType) return model, nil } if err != nil { diff --git a/pkg/fluxApplication/FluxApplicationService.go b/pkg/fluxApplication/FluxApplicationService.go index 779b5b2c63..12a9ec121b 100644 --- a/pkg/fluxApplication/FluxApplicationService.go +++ b/pkg/fluxApplication/FluxApplicationService.go @@ -3,6 +3,9 @@ package fluxApplication import ( "context" "fmt" + "io" + "net/http" + "github.com/devtron-labs/common-lib/utils/k8s/commonBean" "github.com/devtron-labs/devtron/api/connector" "github.com/devtron-labs/devtron/api/helm-app/gRPC" @@ -19,15 +22,15 @@ import ( "github.com/gogo/protobuf/proto" "go.opentelemetry.io/otel" "go.uber.org/zap" - "io" - "net/http" ) type FluxApplicationService interface { - ListFluxApplications(ctx context.Context, clusterIds []int, noStream bool, w http.ResponseWriter) + ListFluxApplications(ctx context.Context, clusterIds []int, noStream bool, w http.ResponseWriter, + token string, fluxAuth func(token string, clusterName string, namespace string, appName string) bool) GetFluxAppDetail(ctx context.Context, app *bean.FluxAppIdentifier) (*bean.FluxApplicationDetailDto, error) HibernateFluxApplication(ctx context.Context, app *bean.FluxAppIdentifier, hibernateRequest *openapi.HibernateRequest) ([]*openapi.HibernateStatus, error) UnHibernateFluxApplication(ctx context.Context, app *bean.FluxAppIdentifier, hibernateRequest *openapi.HibernateRequest) ([]*openapi.HibernateStatus, error) + GetFluxApplicationList(ctx context.Context, clusterIds []int) ([]bean.FluxApplication, error) } type FluxApplicationServiceImpl struct { @@ -92,23 +95,55 @@ func (impl *FluxApplicationServiceImpl) UnHibernateFluxApplication(ctx context.C return response, nil } -func (impl *FluxApplicationServiceImpl) ListFluxApplications(ctx context.Context, clusterIds []int, noStream bool, w http.ResponseWriter) { +func (impl *FluxApplicationServiceImpl) GetFluxApplicationList(ctx context.Context, clusterIds []int) ([]bean.FluxApplication, error) { appStream, err := impl.listApplications(ctx, clusterIds) if err != nil { - impl.logger.Errorw("error in listing flux applications", "clusterIds", clusterIds, "err", err) - return + return nil, err + } + cdPipelineMap, installedAppMap, err := impl.getDevtronManagedMaps(clusterIds) + if err != nil { + return nil, err } + apps := make([]bean.FluxApplication, 0) + for { + appDetail, err := appStream.Recv() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + if appDetail.Errored { + impl.logger.Errorw("error in listing flux applications for cluster, skipping it", + "clusterId", appDetail.ClusterId, "errorMsg", appDetail.ErrorMsg) + continue + } + for _, d := range appDetail.FluxApplication { + key := fmt.Sprintf("%v-%s", d.EnvironmentDetail.ClusterId, d.EnvironmentDetail.Namespace) + if _, ok := cdPipelineMap[key][d.Name]; ok { + continue + } + if _, ok := installedAppMap[key][d.Name]; ok { + continue + } + apps = append(apps, toFluxApplication(d)) + } + } + return apps, nil +} + +func (impl *FluxApplicationServiceImpl) getDevtronManagedMaps(clusterIds []int) (map[string]map[string]bool, map[string]map[string]bool, error) { fluxCdPipelines, err := impl.pipelineRepository.GetAppAndEnvDetailsForDeploymentAppTypePipeline(util.PIPELINE_DEPLOYMENT_TYPE_FLUX, clusterIds) if err != nil { impl.logger.Errorw("error in fetching helm app list from DB created using cd_pipelines", "clusters", clusterIds, "err", err) - return + return nil, nil, err } installedHelmApps, err := impl.installedAppRepository.GetAppAndEnvDetailsForDeploymentAppTypeInstalledApps(util.PIPELINE_DEPLOYMENT_TYPE_FLUX, clusterIds) if err != nil { impl.logger.Errorw("error in fetching helm app list from DB created from app store", "clusters", clusterIds, "err", err) - return + return nil, nil, err } cdPipelineMap := make(map[string]map[string]bool) // map of clusterId-namespace, deploymentAppName @@ -129,52 +164,70 @@ func (impl *FluxApplicationServiceImpl) ListFluxApplications(ctx context.Context deploymentAppName := fmt.Sprintf("%s-%s", i.App.AppName, i.Environment.Namespace) installedAppMap[key][deploymentAppName] = true } + + return cdPipelineMap, installedAppMap, nil +} + +func toFluxApplication(app *gRPC.FluxApplication) bean.FluxApplication { + fluxApp := bean.FluxApplication{ + Name: app.Name, + HealthStatus: app.HealthStatus, + SyncStatus: app.SyncStatus, + ClusterId: int(app.EnvironmentDetail.ClusterId), + ClusterName: app.EnvironmentDetail.ClusterName, + Namespace: app.EnvironmentDetail.Namespace, + FluxAppDeploymentType: app.FluxAppDeploymentType, + } + + return fluxApp +} + +func (impl *FluxApplicationServiceImpl) ListFluxApplications(ctx context.Context, clusterIds []int, noStream bool, w http.ResponseWriter, + token string, fluxAuth func(token string, clusterName string, namespace string, appName string) bool) { + if !noStream { + appStream, err := impl.listApplications(ctx, clusterIds) + if err != nil { + impl.logger.Errorw("error in listing flux applications", "clusterIds", clusterIds, "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + cdPipelineMap, installedAppMap, err := impl.getDevtronManagedMaps(clusterIds) + if err != nil { + impl.logger.Errorw("error in getting devtron managed flux apps", "clusterIds", clusterIds, "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } impl.pump.StartStreamWithTransformer(w, func() (proto.Message, error) { return appStream.Recv() }, err, func(message interface{}) interface{} { - return impl.appListRespProtoTransformer(message.(*gRPC.FluxApplicationList), cdPipelineMap, installedAppMap) + return impl.appListRespProtoTransformer(message.(*gRPC.FluxApplicationList), cdPipelineMap, installedAppMap, token, fluxAuth) }) } else { - fluxApps := make([]bean.FluxApplication, 0) - for { - appDetail, err := appStream.Recv() - if err == io.EOF { - break - } - if err != nil { - return + fluxApps, err := impl.GetFluxApplicationList(ctx, clusterIds) + if err != nil { + impl.logger.Errorw("error in getting flux application list", "clusterIds", clusterIds, "err", err) + errored := true + errMsg := err.Error() + appList := bean.FluxAppList{ + Errored: &errored, + ErrorMsg: &errMsg, } - if appDetail.Errored { - appList := bean.FluxAppList{ - Errored: &appDetail.Errored, - ErrorMsg: &appDetail.ErrorMsg, - } - common.WriteJsonResp(w, nil, appList, http.StatusOK) - return - } else { - for _, deployedApp := range appDetail.FluxApplication { - key := fmt.Sprintf("%v-%s", deployedApp.EnvironmentDetail.ClusterId, deployedApp.EnvironmentDetail.Namespace) - if _, ok := cdPipelineMap[key][deployedApp.Name]; ok { - continue - } - if _, ok := installedAppMap[key][deployedApp.Name]; ok { - continue - } - fluxApp := bean.FluxApplication{ - Name: deployedApp.Name, - HealthStatus: deployedApp.HealthStatus, - SyncStatus: deployedApp.SyncStatus, - ClusterId: int(deployedApp.EnvironmentDetail.ClusterId), - ClusterName: deployedApp.EnvironmentDetail.ClusterName, - Namespace: deployedApp.EnvironmentDetail.Namespace, - FluxAppDeploymentType: deployedApp.FluxAppDeploymentType, - } - fluxApps = append(fluxApps, fluxApp) + common.WriteJsonResp(w, nil, appList, http.StatusOK) + return + } + + if fluxAuth != nil { + authorised := make([]bean.FluxApplication, 0, len(fluxApps)) + for _, app := range fluxApps { + if fluxAuth(token, app.ClusterName, app.Namespace, app.Name) { + authorised = append(authorised, app) } } + fluxApps = authorised } + //RBAC enforcer Ends clusterIdsInt32 := sliceUtil.NewSliceFromFuncExec(clusterIds, func(clusterId int) int32 { return int32(clusterId) }) @@ -234,6 +287,11 @@ func (impl *FluxApplicationServiceImpl) listApplications(ctx context.Context, cl } for _, clusterDetail := range clusters { + if clusterDetail.IsVirtualCluster || len(clusterDetail.ErrorInConnecting) != 0 { + impl.logger.Debugw("skipping cluster for flux app listing", "clusterId", clusterDetail.Id, + "isVirtualCluster", clusterDetail.IsVirtualCluster, "errorInConnecting", clusterDetail.ErrorInConnecting) + continue + } config := &gRPC.ClusterConfig{ ApiServerUrl: clusterDetail.ServerUrl, Token: clusterDetail.Config[commonBean.BearerToken], @@ -252,7 +310,8 @@ func (impl *FluxApplicationServiceImpl) listApplications(ctx context.Context, cl return applicationStream, err } -func (impl *FluxApplicationServiceImpl) appListRespProtoTransformer(deployedApps *gRPC.FluxApplicationList, fluxCdPipelines map[string]map[string]bool, fluxInstalledApps map[string]map[string]bool) bean.FluxAppList { +func (impl *FluxApplicationServiceImpl) appListRespProtoTransformer(deployedApps *gRPC.FluxApplicationList, fluxCdPipelines map[string]map[string]bool, fluxInstalledApps map[string]map[string]bool, + token string, fluxAuth func(token string, clusterName string, namespace string, appName string) bool) bean.FluxAppList { appList := bean.FluxAppList{ClusterId: &[]int32{deployedApps.ClusterId}} if deployedApps.Errored { @@ -268,6 +327,11 @@ func (impl *FluxApplicationServiceImpl) appListRespProtoTransformer(deployedApps if _, ok := fluxInstalledApps[key][deployedApp.Name]; ok { continue } + if fluxAuth != nil && !fluxAuth(token, deployedApp.EnvironmentDetail.ClusterName, + deployedApp.EnvironmentDetail.Namespace, deployedApp.Name) { + continue + } + //RBAC enforcer Ends fluxApp := bean.FluxApplication{ Name: deployedApp.Name, HealthStatus: deployedApp.HealthStatus, diff --git a/pkg/userResource/UserResourceExtendedService.go b/pkg/userResource/UserResourceExtendedService.go index 358a5000e3..8d78cbb741 100644 --- a/pkg/userResource/UserResourceExtendedService.go +++ b/pkg/userResource/UserResourceExtendedService.go @@ -2,14 +2,18 @@ package userResource import ( "context" + "net/http" + apiBean "github.com/devtron-labs/devtron/api/userResource/bean" "github.com/devtron-labs/devtron/internal/util" "github.com/devtron-labs/devtron/pkg/app" "github.com/devtron-labs/devtron/pkg/appStore/chartGroup" "github.com/devtron-labs/devtron/pkg/appWorkflow" + argoApplication2 "github.com/devtron-labs/devtron/pkg/argoApplication" "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin" "github.com/devtron-labs/devtron/pkg/cluster" "github.com/devtron-labs/devtron/pkg/cluster/environment" + "github.com/devtron-labs/devtron/pkg/fluxApplication" application2 "github.com/devtron-labs/devtron/pkg/k8s/application" "github.com/devtron-labs/devtron/pkg/team" "github.com/devtron-labs/devtron/pkg/userResource/adapter" @@ -18,7 +22,6 @@ import ( "github.com/devtron-labs/devtron/util/commonEnforcementFunctionsUtil" "github.com/devtron-labs/devtron/util/rbac" "go.uber.org/zap" - "net/http" ) type UserResourceExtendedServiceImpl struct { @@ -39,13 +42,15 @@ func NewUserResourceExtendedServiceImpl(logger *zap.SugaredLogger, teamService t clusterService cluster.ClusterService, rbacEnforcementUtil commonEnforcementFunctionsUtil.CommonEnforcementUtil, enforcerUtil rbac.EnforcerUtil, - enforcer casbin.Enforcer) *UserResourceExtendedServiceImpl { + enforcer casbin.Enforcer, + argoService argoApplication2.ArgoApplicationService, + fluxService fluxApplication.FluxApplicationService) *UserResourceExtendedServiceImpl { return &UserResourceExtendedServiceImpl{ logger: logger, chartGroupService: chartGroupService, appListingService: appListingService, appWorkflowService: appWorkflowService, - UserResourceServiceImpl: NewUserResourceServiceImpl(logger, teamService, envService, clusterService, k8sApplicationService, enforcerUtil, rbacEnforcementUtil, enforcer, appService), + UserResourceServiceImpl: NewUserResourceServiceImpl(logger, teamService, envService, clusterService, k8sApplicationService, enforcerUtil, rbacEnforcementUtil, enforcer, appService, argoService, fluxService), } } diff --git a/pkg/userResource/UserResourceRbacExtendedService.go b/pkg/userResource/UserResourceRbacExtendedService.go index 0f6c30205d..eb146d5c3e 100644 --- a/pkg/userResource/UserResourceRbacExtendedService.go +++ b/pkg/userResource/UserResourceRbacExtendedService.go @@ -65,6 +65,24 @@ func (impl *UserResourceServiceImpl) enforceRbacForHelmAppsListing(token string, return adapter.BuildUserResourceResponseDto(resourceOptions.TeamAppResp), nil } +func (impl *UserResourceServiceImpl) enforceRbacForArgoAppsListing(token string, params *apiBean.PathParams, resourceOptions *bean.ResourceOptionsDto) (*bean.UserResourceResponseDto, error) { + isAuthorised := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionUpdate, "*") + if !isAuthorised { + impl.logger.Errorw("user is unauthorized enforceRbacForArgoAppsListing") + return adapter.BuildNullDataUserResourceResponseDto(), nil + } + return adapter.BuildUserResourceResponseDto(resourceOptions.ExternalGitOpsAppResp), nil +} + +func (impl *UserResourceServiceImpl) enforceRbacForFluxAppsListing(token string, params *apiBean.PathParams, resourceOptions *bean.ResourceOptionsDto) (*bean.UserResourceResponseDto, error) { + isAuthorised := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionUpdate, "*") + if !isAuthorised { + impl.logger.Errorw("user is unauthorized enforceRbacForFluxAppsListing") + return adapter.BuildNullDataUserResourceResponseDto(), nil + } + return adapter.BuildUserResourceResponseDto(resourceOptions.ExternalGitOpsAppResp), nil +} + func (impl *UserResourceServiceImpl) enforceRbacForJobs(token string, params *apiBean.PathParams, resourceOptions *bean.ResourceOptionsDto) (*bean.UserResourceResponseDto, error) { isAuthorised := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionUpdate, "*") if !isAuthorised { diff --git a/pkg/userResource/UserResourceService.go b/pkg/userResource/UserResourceService.go index 48274c5fe8..49d44e273d 100644 --- a/pkg/userResource/UserResourceService.go +++ b/pkg/userResource/UserResourceService.go @@ -2,23 +2,27 @@ package userResource import ( "context" + "net/http" + apiBean "github.com/devtron-labs/devtron/api/userResource/bean" app2 "github.com/devtron-labs/devtron/internal/sql/repository/app" "github.com/devtron-labs/devtron/internal/util" "github.com/devtron-labs/devtron/pkg/app" + argoApplication2 "github.com/devtron-labs/devtron/pkg/argoApplication" "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin" "github.com/devtron-labs/devtron/pkg/auth/user/bean" "github.com/devtron-labs/devtron/pkg/cluster" "github.com/devtron-labs/devtron/pkg/cluster/environment" + "github.com/devtron-labs/devtron/pkg/fluxApplication" application2 "github.com/devtron-labs/devtron/pkg/k8s/application" bean4 "github.com/devtron-labs/devtron/pkg/k8s/bean" "github.com/devtron-labs/devtron/pkg/team" + "github.com/devtron-labs/devtron/pkg/userResource/adapter" bean5 "github.com/devtron-labs/devtron/pkg/userResource/bean" "github.com/devtron-labs/devtron/pkg/userResource/helper" "github.com/devtron-labs/devtron/util/commonEnforcementFunctionsUtil" "github.com/devtron-labs/devtron/util/rbac" "go.uber.org/zap" - "net/http" ) type UserResourceService interface { @@ -26,15 +30,17 @@ type UserResourceService interface { params *apiBean.PathParams) (*bean5.UserResourceResponseDto, error) } type UserResourceServiceImpl struct { - logger *zap.SugaredLogger - teamService team.TeamService - envService environment.EnvironmentService - clusterService cluster.ClusterService - k8sApplicationService application2.K8sApplicationService - enforcerUtil rbac.EnforcerUtil - rbacEnforcementUtil commonEnforcementFunctionsUtil.CommonEnforcementUtil - enforcer casbin.Enforcer - appService app.AppCrudOperationService + logger *zap.SugaredLogger + teamService team.TeamService + envService environment.EnvironmentService + clusterService cluster.ClusterService + k8sApplicationService application2.K8sApplicationService + enforcerUtil rbac.EnforcerUtil + rbacEnforcementUtil commonEnforcementFunctionsUtil.CommonEnforcementUtil + enforcer casbin.Enforcer + appService app.AppCrudOperationService + argoApplicationService argoApplication2.ArgoApplicationService + fluxApplicationService fluxApplication.FluxApplicationService } func NewUserResourceServiceImpl(logger *zap.SugaredLogger, @@ -45,17 +51,21 @@ func NewUserResourceServiceImpl(logger *zap.SugaredLogger, enforcerUtil rbac.EnforcerUtil, rbacEnforcementUtil commonEnforcementFunctionsUtil.CommonEnforcementUtil, enforcer casbin.Enforcer, - appService app.AppCrudOperationService) *UserResourceServiceImpl { + appService app.AppCrudOperationService, + argoApplicationService argoApplication2.ArgoApplicationService, + fluxApplicationService fluxApplication.FluxApplicationService) *UserResourceServiceImpl { return &UserResourceServiceImpl{ - logger: logger, - teamService: teamService, - envService: envService, - clusterService: clusterService, - k8sApplicationService: k8sApplicationService, - enforcerUtil: enforcerUtil, - rbacEnforcementUtil: rbacEnforcementUtil, - enforcer: enforcer, - appService: appService, + logger: logger, + teamService: teamService, + envService: envService, + clusterService: clusterService, + k8sApplicationService: k8sApplicationService, + enforcerUtil: enforcerUtil, + rbacEnforcementUtil: rbacEnforcementUtil, + enforcer: enforcer, + appService: appService, + argoApplicationService: argoApplicationService, + fluxApplicationService: fluxApplicationService, } } @@ -121,7 +131,43 @@ func (impl *UserResourceServiceImpl) getHelmAppResourceOptions(context context.C return bean5.NewResourceOptionsDto().WithTeamAppResp(apps), nil } -func (impl *UserResourceServiceImpl) getHelmEnvResourceOptions(context context.Context, token string, +func (impl *UserResourceServiceImpl) getArgoAppResourceOptions(context context.Context, token string, + reqBean *apiBean.ResourceOptionsReqDto, params *apiBean.PathParams) (*bean5.ResourceOptionsDto, error) { + clusterIds, err := helper.GetValidatedClusterIds(reqBean) + if err != nil { + impl.logger.Errorw("error encountered in getArgoAppResourceOptions", "err", err) + return nil, err + } + apps, err := impl.argoApplicationService.ListApplications(clusterIds) + if err != nil { + impl.logger.Errorw("error encountered in getArgoAppResourceOptions", "err", err) + return nil, err + } + appDtos := helper.FilterExternalGitOpsAppsByEnvIdentifier( + adapter.ArgoAppToExternalGitOpsApp(apps), reqBean.EnvironmentIdentifiers) + + return bean5.NewResourceOptionsDto().WithExternalGitOpsAppResp(appDtos), nil +} + +func (impl *UserResourceServiceImpl) getFluxAppResourceOptions(context context.Context, token string, + reqBean *apiBean.ResourceOptionsReqDto, params *apiBean.PathParams) (*bean5.ResourceOptionsDto, error) { + clusterIds, err := helper.GetValidatedClusterIds(reqBean) + if err != nil { + impl.logger.Errorw("error encountered in getFluxAppResourceOptions", "err", err) + return nil, err + } + apps, err := impl.fluxApplicationService.GetFluxApplicationList(context, clusterIds) + if err != nil { + impl.logger.Errorw("error encountered in getFluxAppResourceOptions", "err", err) + return nil, err + } + appDtos := helper.FilterExternalGitOpsAppsByEnvIdentifier( + adapter.FluxAppToExternalGitOpsApp(apps), reqBean.EnvironmentIdentifiers) + + return bean5.NewResourceOptionsDto().WithExternalGitOpsAppResp(appDtos), nil +} + +func (impl *UserResourceServiceImpl) getCombinedEnvResourceOptions(context context.Context, token string, reqBean *apiBean.ResourceOptionsReqDto, params *apiBean.PathParams) (*bean5.ResourceOptionsDto, error) { // get helm env resource options diff --git a/pkg/userResource/adapter/adapter.go b/pkg/userResource/adapter/adapter.go index 874b61b8f2..73b30b9a3c 100644 --- a/pkg/userResource/adapter/adapter.go +++ b/pkg/userResource/adapter/adapter.go @@ -4,6 +4,8 @@ import ( bean2 "github.com/devtron-labs/devtron/api/userResource/bean" "github.com/devtron-labs/devtron/internal/sql/repository/helper" "github.com/devtron-labs/devtron/pkg/app" + bean3 "github.com/devtron-labs/devtron/pkg/argoApplication/bean" + bean4 "github.com/devtron-labs/devtron/pkg/fluxApplication/bean" "github.com/devtron-labs/devtron/pkg/userResource/bean" ) @@ -26,3 +28,33 @@ func BuildFetchAppListingReqForJobFromDto(reqBean *bean2.ResourceOptionsReqDto) SortOrder: helper.Asc, // default values set } } + +func ArgoAppToExternalGitOpsApp(applications []*bean3.ArgoApplicationListDto) []*bean.ExternalGitOpsAppDto { + result := make([]*bean.ExternalGitOpsAppDto, 0, len(applications)) + for _, application := range applications { + appDto := bean.ExternalGitOpsAppDto{ + AppName: application.Name, + Namespace: application.Namespace, + ClusterId: application.ClusterId, + ClusterName: application.ClusterName, + } + result = append(result, &appDto) + } + + return result +} + +func FluxAppToExternalGitOpsApp(applications []bean4.FluxApplication) []*bean.ExternalGitOpsAppDto { + result := make([]*bean.ExternalGitOpsAppDto, 0, len(applications)) + for _, application := range applications { + appDto := bean.ExternalGitOpsAppDto{ + AppName: application.Name, + Namespace: application.Namespace, + ClusterId: application.ClusterId, + ClusterName: application.ClusterName, + } + result = append(result, &appDto) + } + + return result +} diff --git a/pkg/userResource/bean/bean.go b/pkg/userResource/bean/bean.go index 03f07bd071..7d085bb8dd 100644 --- a/pkg/userResource/bean/bean.go +++ b/pkg/userResource/bean/bean.go @@ -15,17 +15,25 @@ type UserResourceResponseDto struct { Data interface{} `json:"data"` } type ResourceOptionsDto struct { - TeamsResp []bean2.TeamRequest - HelmEnvResp []*bean.ClusterEnvDto - ClusterResp []bean3.ClusterBean - NameSpaces []string - ApiResourcesResp *k8s.GetAllApiResourcesResponse - ClusterResourcesResp *k8s.ClusterResourceListMap - TeamAppResp []*app.TeamAppBean - EnvResp []bean.EnvironmentBean - ChartGroupResp *chartGroup.ChartGroupList - JobsResp []*AppView.JobContainer - AppWfsResp *bean4.WorkflowNamesResponse + TeamsResp []bean2.TeamRequest + HelmEnvResp []*bean.ClusterEnvDto + ClusterResp []bean3.ClusterBean + NameSpaces []string + ApiResourcesResp *k8s.GetAllApiResourcesResponse + ClusterResourcesResp *k8s.ClusterResourceListMap + TeamAppResp []*app.TeamAppBean + EnvResp []bean.EnvironmentBean + ChartGroupResp *chartGroup.ChartGroupList + JobsResp []*AppView.JobContainer + AppWfsResp *bean4.WorkflowNamesResponse + ExternalGitOpsAppResp []*ExternalGitOpsAppDto +} + +type ExternalGitOpsAppDto struct { + AppName string `json:"appName"` + Namespace string `json:"namespace"` + ClusterId int `json:"clusterId"` + ClusterName string `json:"clusterName"` } func NewResourceOptionsDto() *ResourceOptionsDto { @@ -78,6 +86,11 @@ func (r *ResourceOptionsDto) WithAppWfsResp(appWfsResp *bean4.WorkflowNamesRespo return r } +func (r *ResourceOptionsDto) WithExternalGitOpsAppResp(externalGitOpsAppResp []*ExternalGitOpsAppDto) *ResourceOptionsDto { + r.ExternalGitOpsAppResp = externalGitOpsAppResp + return r +} + type Version string type UserResourceKind string @@ -95,6 +108,10 @@ const ( ClusterNamespaces UserResourceKind = "cluster/namespaces" ClusterApiResources UserResourceKind = "cluster/apiResources" ClusterResources UserResourceKind = "cluster/resources" + KindArgoEnvironment UserResourceKind = "environment/argo" + KindFluxEnvironment UserResourceKind = "environment/flux" + KindArgoApplication UserResourceKind = Application + "/argo" + KindFluxApplication UserResourceKind = Application + "/flux" ) const ( diff --git a/pkg/userResource/bean/messages.go b/pkg/userResource/bean/messages.go index b71c8c199e..27571ddf96 100644 --- a/pkg/userResource/bean/messages.go +++ b/pkg/userResource/bean/messages.go @@ -1,8 +1,9 @@ package bean const ( - InvalidPayloadMessage = "Invalid Payload" - InvalidEntityMessage = "Invalid Entity" + InvalidPayloadMessage = "Invalid Payload" + InvalidEntityMessage = "Invalid Entity" + InvalidClusterIdMessage = "Invalid clusterId" ) // messages diff --git a/pkg/userResource/helper/helper.go b/pkg/userResource/helper/helper.go index 58251e3c91..4fe89415b2 100644 --- a/pkg/userResource/helper/helper.go +++ b/pkg/userResource/helper/helper.go @@ -1,10 +1,13 @@ package helper import ( + "fmt" + "net/http" + "strings" + apiBean "github.com/devtron-labs/devtron/api/userResource/bean" "github.com/devtron-labs/devtron/internal/util" bean5 "github.com/devtron-labs/devtron/pkg/userResource/bean" - "net/http" ) func ValidateResourceOptionReqBean(reqBean *apiBean.ResourceOptionsReqDto) error { @@ -16,3 +19,50 @@ func ValidateResourceOptionReqBean(reqBean *apiBean.ResourceOptionsReqDto) error } return nil } + +func GetValidatedClusterIds(reqBean *apiBean.ResourceOptionsReqDto) ([]int, error) { + invalid := util.GetApiErrorAdapter(http.StatusBadRequest, "400", + bean5.InvalidClusterIdMessage, bean5.InvalidClusterIdMessage) + if reqBean == nil { + return nil, invalid + } + seen := make(map[int]bool, len(reqBean.ClusterIds)+1) + clusterIds := make([]int, 0, len(reqBean.ClusterIds)+1) + appendIfValid := func(clusterId int) { + if clusterId > 0 && !seen[clusterId] { + seen[clusterId] = true + clusterIds = append(clusterIds, clusterId) + } + } + for _, clusterId := range reqBean.ClusterIds { + appendIfValid(clusterId) + } + if reqBean.ResourceRequestBean != nil { + appendIfValid(reqBean.ClusterId) + } + if len(clusterIds) == 0 { + return nil, invalid + } + return clusterIds, nil +} + +func FilterExternalGitOpsAppsByEnvIdentifier(apps []*bean5.ExternalGitOpsAppDto, envIdentifiers []string) []*bean5.ExternalGitOpsAppDto { + if len(envIdentifiers) == 0 { + return apps + } + selected := make(map[string]bool, len(envIdentifiers)) + for _, identifier := range envIdentifiers { + selected[strings.ToLower(identifier)] = true + } + filtered := make([]*bean5.ExternalGitOpsAppDto, 0, len(apps)) + for _, app := range apps { + if app == nil { + continue + } + identifier := fmt.Sprintf("%s__%s", app.ClusterName, app.Namespace) + if selected[strings.ToLower(identifier)] { + filtered = append(filtered, app) + } + } + return filtered +} diff --git a/pkg/userResource/logicRouteService.go b/pkg/userResource/logicRouteService.go index 9b9b41ab2c..63560e3a9f 100644 --- a/pkg/userResource/logicRouteService.go +++ b/pkg/userResource/logicRouteService.go @@ -3,6 +3,7 @@ package userResource import ( "context" "fmt" + apiBean "github.com/devtron-labs/devtron/api/userResource/bean" bean2 "github.com/devtron-labs/devtron/pkg/auth/user/bean" "github.com/devtron-labs/devtron/pkg/userResource/bean" @@ -17,12 +18,16 @@ func getUserResourceKindWithVersionKey(kind bean.UserResourceKind, version bean. var mapOfUserResourceKindToAllResourceOptionsFunc = map[string]func(impl *UserResourceServiceImpl, context context.Context, token string, reqBean *apiBean.ResourceOptionsReqDto, params *apiBean.PathParams) (*bean.ResourceOptionsDto, error){ getUserResourceKindWithVersionKey(bean.KindTeam, bean.Alpha1Version): (*UserResourceServiceImpl).getTeamResourceOptions, - getUserResourceKindWithVersionKey(bean.KindHelmEnvironment, bean.Alpha1Version): (*UserResourceServiceImpl).getHelmEnvResourceOptions, + getUserResourceKindWithVersionKey(bean.KindHelmEnvironment, bean.Alpha1Version): (*UserResourceServiceImpl).getCombinedEnvResourceOptions, getUserResourceKindWithVersionKey(bean.KindHelmApplication, bean.Alpha1Version): (*UserResourceServiceImpl).getHelmAppResourceOptions, getUserResourceKindWithVersionKey(bean.KindCluster, bean.Alpha1Version): (*UserResourceServiceImpl).getClusterResourceOptions, getUserResourceKindWithVersionKey(bean.ClusterApiResources, bean.Alpha1Version): (*UserResourceServiceImpl).getClusterApiResourceOptions, getUserResourceKindWithVersionKey(bean.ClusterNamespaces, bean.Alpha1Version): (*UserResourceServiceImpl).getClusterNamespacesResourceOptions, getUserResourceKindWithVersionKey(bean.ClusterResources, bean.Alpha1Version): (*UserResourceServiceImpl).getClusterResourceListOptions, + getUserResourceKindWithVersionKey(bean.KindArgoEnvironment, bean.Alpha1Version): (*UserResourceServiceImpl).getCombinedEnvResourceOptions, + getUserResourceKindWithVersionKey(bean.KindFluxEnvironment, bean.Alpha1Version): (*UserResourceServiceImpl).getCombinedEnvResourceOptions, + getUserResourceKindWithVersionKey(bean.KindArgoApplication, bean.Alpha1Version): (*UserResourceServiceImpl).getArgoAppResourceOptions, + getUserResourceKindWithVersionKey(bean.KindFluxApplication, bean.Alpha1Version): (*UserResourceServiceImpl).getFluxAppResourceOptions, } func getAllResourceOptionsFunc(kind bean.UserResourceKind, version bean.Version) func(impl *UserResourceServiceImpl, context context.Context, token string, reqBean *apiBean.ResourceOptionsReqDto, params *apiBean.PathParams) (*bean.ResourceOptionsDto, error) { @@ -34,7 +39,7 @@ func getAllResourceOptionsFunc(kind bean.UserResourceKind, version bean.Version) var mapOfUserResourceKindToAllResourceOptionsExtendedFunc = map[string]func(impl *UserResourceExtendedServiceImpl, context context.Context, token string, reqBean *apiBean.ResourceOptionsReqDto, params *apiBean.PathParams) (*bean.ResourceOptionsDto, error){ getUserResourceKindWithVersionKey(bean.KindTeam, bean.Alpha1Version): (*UserResourceExtendedServiceImpl).getTeamResourceOptions, - getUserResourceKindWithVersionKey(bean.KindHelmEnvironment, bean.Alpha1Version): (*UserResourceExtendedServiceImpl).getHelmEnvResourceOptions, + getUserResourceKindWithVersionKey(bean.KindHelmEnvironment, bean.Alpha1Version): (*UserResourceExtendedServiceImpl).getCombinedEnvResourceOptions, getUserResourceKindWithVersionKey(bean.KindHelmApplication, bean.Alpha1Version): (*UserResourceExtendedServiceImpl).getHelmAppResourceOptions, getUserResourceKindWithVersionKey(bean.KindCluster, bean.Alpha1Version): (*UserResourceExtendedServiceImpl).getClusterResourceOptions, getUserResourceKindWithVersionKey(bean.ClusterApiResources, bean.Alpha1Version): (*UserResourceExtendedServiceImpl).getClusterApiResourceOptions, @@ -45,6 +50,10 @@ var mapOfUserResourceKindToAllResourceOptionsExtendedFunc = map[string]func(impl getUserResourceKindWithVersionKey(bean.KindChartGroup, bean.Alpha1Version): (*UserResourceExtendedServiceImpl).getChartGroupResourceOptions, getUserResourceKindWithVersionKey(bean.KindJobs, bean.Alpha1Version): (*UserResourceExtendedServiceImpl).getJobsResourceOptions, getUserResourceKindWithVersionKey(bean.KindWorkflow, bean.Alpha1Version): (*UserResourceExtendedServiceImpl).getAppWfsResourceOptions, + getUserResourceKindWithVersionKey(bean.KindArgoEnvironment, bean.Alpha1Version): (*UserResourceExtendedServiceImpl).getCombinedEnvResourceOptions, + getUserResourceKindWithVersionKey(bean.KindFluxEnvironment, bean.Alpha1Version): (*UserResourceExtendedServiceImpl).getCombinedEnvResourceOptions, + getUserResourceKindWithVersionKey(bean.KindArgoApplication, bean.Alpha1Version): (*UserResourceExtendedServiceImpl).getArgoAppResourceOptions, + getUserResourceKindWithVersionKey(bean.KindFluxApplication, bean.Alpha1Version): (*UserResourceExtendedServiceImpl).getFluxAppResourceOptions, } func getAllResourceOptionsExtendedFunc(kind bean.UserResourceKind, version bean.Version) func(impl *UserResourceExtendedServiceImpl, context context.Context, token string, reqBean *apiBean.ResourceOptionsReqDto, params *apiBean.PathParams) (*bean.ResourceOptionsDto, error) { @@ -62,6 +71,10 @@ var mapOfKindWithEntityAccessTypeKeyToResourceOptionRbacFunc = map[string]func(i getUserResourceKindWithEntityAccessKey(bean.ClusterApiResources, bean.Alpha1Version, bean2.CLUSTER_ENTITIY, bean2.EmptyAccessType): (*UserResourceServiceImpl).enforceRbacForClusterApiResource, getUserResourceKindWithEntityAccessKey(bean.ClusterNamespaces, bean.Alpha1Version, bean2.CLUSTER_ENTITIY, bean2.EmptyAccessType): (*UserResourceServiceImpl).enforceRbacForClusterNamespaces, getUserResourceKindWithEntityAccessKey(bean.ClusterResources, bean.Alpha1Version, bean2.CLUSTER_ENTITIY, bean2.EmptyAccessType): (*UserResourceServiceImpl).enforceRbacForClusterResourceList, + getUserResourceKindWithEntityAccessKey(bean.KindArgoEnvironment, bean.Alpha1Version, bean2.ENTITY_APPS, bean2.APP_ACCESS_TYPE_ARGO): (*UserResourceServiceImpl).enforceRbacForEnvForHelmApp, + getUserResourceKindWithEntityAccessKey(bean.KindFluxEnvironment, bean.Alpha1Version, bean2.ENTITY_APPS, bean2.APP_ACCESS_TYPE_FLUX): (*UserResourceServiceImpl).enforceRbacForEnvForHelmApp, + getUserResourceKindWithEntityAccessKey(bean.KindArgoApplication, bean.Alpha1Version, bean2.ENTITY_APPS, bean2.APP_ACCESS_TYPE_ARGO): (*UserResourceServiceImpl).enforceRbacForArgoAppsListing, + getUserResourceKindWithEntityAccessKey(bean.KindFluxApplication, bean.Alpha1Version, bean2.ENTITY_APPS, bean2.APP_ACCESS_TYPE_FLUX): (*UserResourceServiceImpl).enforceRbacForFluxAppsListing, } func getResourceOptionRbacFunc(kind bean.UserResourceKind, version bean.Version, entity string, accessType string) func(impl *UserResourceServiceImpl, token string, params *apiBean.PathParams, resourceOptions *bean.ResourceOptionsDto) (*bean.UserResourceResponseDto, error) { @@ -87,6 +100,10 @@ var mapOfKindWithEntityAccessTypeKeyToResourceOptionRbacExtendedFunc = map[strin getUserResourceKindWithEntityAccessKey(bean.KindChartGroup, bean.Alpha1Version, bean2.CHART_GROUP_ENTITY, bean2.EmptyAccessType): (*UserResourceExtendedServiceImpl).enforceRbacForChartGroup, getUserResourceKindWithEntityAccessKey(bean.KindJobs, bean.Alpha1Version, bean2.EntityJobs, bean2.EmptyAccessType): (*UserResourceExtendedServiceImpl).enforceRbacForJobs, getUserResourceKindWithEntityAccessKey(bean.KindWorkflow, bean.Alpha1Version, bean2.EntityJobs, bean2.EmptyAccessType): (*UserResourceExtendedServiceImpl).enforceRbacForJobsWfs, + getUserResourceKindWithEntityAccessKey(bean.KindArgoEnvironment, bean.Alpha1Version, bean2.ENTITY_APPS, bean2.APP_ACCESS_TYPE_ARGO): (*UserResourceExtendedServiceImpl).enforceRbacForEnvForHelmApp, + getUserResourceKindWithEntityAccessKey(bean.KindFluxEnvironment, bean.Alpha1Version, bean2.ENTITY_APPS, bean2.APP_ACCESS_TYPE_FLUX): (*UserResourceExtendedServiceImpl).enforceRbacForEnvForHelmApp, + getUserResourceKindWithEntityAccessKey(bean.KindArgoApplication, bean.Alpha1Version, bean2.ENTITY_APPS, bean2.APP_ACCESS_TYPE_ARGO): (*UserResourceExtendedServiceImpl).enforceRbacForArgoAppsListing, + getUserResourceKindWithEntityAccessKey(bean.KindFluxApplication, bean.Alpha1Version, bean2.ENTITY_APPS, bean2.APP_ACCESS_TYPE_FLUX): (*UserResourceExtendedServiceImpl).enforceRbacForFluxAppsListing, } func getResourceOptionRbacExtendedFunc(kind bean.UserResourceKind, version bean.Version, entity string, accessType string) func(impl *UserResourceExtendedServiceImpl, token string, params *apiBean.PathParams, resourceOptions *bean.ResourceOptionsDto) (*bean.UserResourceResponseDto, error) { diff --git a/scripts/casbin/12_argo_flux_rbac.down.sql b/scripts/casbin/12_argo_flux_rbac.down.sql new file mode 100644 index 0000000000..59b12b5f86 --- /dev/null +++ b/scripts/casbin/12_argo_flux_rbac.down.sql @@ -0,0 +1,2 @@ +DELETE FROM casbin_rule WHERE v0='role:super-admin___' AND v1='argo-app'; +DELETE FROM casbin_rule WHERE v0='role:super-admin___' AND v1='flux-app'; \ No newline at end of file diff --git a/scripts/casbin/12_argo_flux_rbac.up.sql b/scripts/casbin/12_argo_flux_rbac.up.sql new file mode 100644 index 0000000000..17e4b72057 --- /dev/null +++ b/scripts/casbin/12_argo_flux_rbac.up.sql @@ -0,0 +1,3 @@ +INSERT INTO "public"."casbin_rule" ("p_type","v0","v1","v2","v3","v4","v5") VALUES + ('p','role:super-admin___','argo-app','*','*','allow',''), + ('p','role:super-admin___','flux-app','*','*','allow',''); \ No newline at end of file diff --git a/scripts/sql/36304600_argo_flux_rbac.down.sql b/scripts/sql/36304600_argo_flux_rbac.down.sql new file mode 100644 index 0000000000..57d8b04829 --- /dev/null +++ b/scripts/sql/36304600_argo_flux_rbac.down.sql @@ -0,0 +1,2 @@ +DELETE FROM "public"."rbac_policy_data" WHERE entity='apps' AND access_type IN ('argo-app','flux-app'); +DELETE FROM "public"."rbac_role_data" WHERE entity='apps' AND access_type IN ('argo-app','flux-app'); \ No newline at end of file diff --git a/scripts/sql/36304600_argo_flux_rbac.up.sql b/scripts/sql/36304600_argo_flux_rbac.up.sql new file mode 100644 index 0000000000..c7ce5e8a0c --- /dev/null +++ b/scripts/sql/36304600_argo_flux_rbac.up.sql @@ -0,0 +1,111 @@ +INSERT INTO "public"."rbac_policy_data" +("entity","access_type","role","policy_data", + "created_on","created_by","updated_on","updated_by","is_preset_role","deleted") +VALUES + ('apps','argo-app','view','{ + "type": { "value": "p", "indexKeyMap": {} }, + "sub": { "value": "argo-app:view_%_%", + "indexKeyMap": { "14": "Env", "16": "App" } }, + "resActObjSet": [ + { "res": { "value": "argo-app", "indexKeyMap": {} }, + "act": { "value": "get", "indexKeyMap": {} }, + "obj": { "value": "%/%", + "indexKeyMap": { "0": "EnvObj", "2": "AppObj" } } }, + { "res": { "value": "global-environment", "indexKeyMap": {} }, + "act": { "value": "get", "indexKeyMap": {} }, + "obj": { "value": "%", "indexKeyMap": { "0": "EnvObj" } } } + ] +}','now()','1','now()','1',true,false), + + ('apps','argo-app','admin','{ + "type": { "value": "p", "indexKeyMap": {} }, + "sub": { "value": "argo-app:admin_%_%", + "indexKeyMap": { "15": "Env", "17": "App" } }, + "resActObjSet": [ + { "res": { "value": "argo-app", "indexKeyMap": {} }, + "act": { "value": "*", "indexKeyMap": {} }, + "obj": { "value": "%/%", + "indexKeyMap": { "0": "EnvObj", "2": "AppObj" } } }, + { "res": { "value": "global-environment", "indexKeyMap": {} }, + "act": { "value": "get", "indexKeyMap": {} }, + "obj": { "value": "%", "indexKeyMap": { "0": "EnvObj" } } } + ] +}','now()','1','now()','1',true,false), + + ('apps','flux-app','view','{ + "type": { "value": "p", "indexKeyMap": {} }, + "sub": { "value": "flux-app:view_%_%", + "indexKeyMap": { "14": "Env", "16": "App" } }, + "resActObjSet": [ + { "res": { "value": "flux-app", "indexKeyMap": {} }, + "act": { "value": "get", "indexKeyMap": {} }, + "obj": { "value": "%/%", + "indexKeyMap": { "0": "EnvObj", "2": "AppObj" } } }, + { "res": { "value": "global-environment", "indexKeyMap": {} }, + "act": { "value": "get", "indexKeyMap": {} }, + "obj": { "value": "%", "indexKeyMap": { "0": "EnvObj" } } } + ] +}','now()','1','now()','1',true,false), + + ('apps','flux-app','admin','{ + "type": { "value": "p", "indexKeyMap": {} }, + "sub": { "value": "flux-app:admin_%_%", + "indexKeyMap": { "15": "Env", "17": "App" } }, + "resActObjSet": [ + { "res": { "value": "flux-app", "indexKeyMap": {} }, + "act": { "value": "*", "indexKeyMap": {} }, + "obj": { "value": "%/%", + "indexKeyMap": { "0": "EnvObj", "2": "AppObj" } } }, + { "res": { "value": "global-environment", "indexKeyMap": {} }, + "act": { "value": "get", "indexKeyMap": {} }, + "obj": { "value": "%", "indexKeyMap": { "0": "EnvObj" } } } + ] +}','now()','1','now()','1',true,false); + +INSERT INTO "public"."rbac_role_data" +("entity","access_type","role","role_display_name","role_description","role_data", + "created_on","created_by","updated_on","updated_by","is_preset_role","deleted") +VALUES + ('apps','argo-app','view','View only', + 'Can view selected Argo CD application(s) and resource manifests of selected application(s)','{ + "role": { "value": "argo-app:view_%_%", + "indexKeyMap": { "14": "Env", "16": "App" } }, + "entityName": { "value": "%", "indexKeyMap": { "0": "App" } }, + "environment":{ "value": "%", "indexKeyMap": { "0": "Env" } }, + "action": { "value": "view", "indexKeyMap": {} }, + "entity": { "value": "%", "indexKeyMap": { "0": "Entity" } }, + "accessType": { "value": "argo-app", "indexKeyMap": {} } +}','now()','1','now()','1',true,false), + + ('apps','argo-app','admin','Admin', + 'Complete access on selected Argo CD application(s)','{ + "role": { "value": "argo-app:admin_%_%", + "indexKeyMap": { "15": "Env", "17": "App" } }, + "entityName": { "value": "%", "indexKeyMap": { "0": "App" } }, + "environment":{ "value": "%", "indexKeyMap": { "0": "Env" } }, + "action": { "value": "admin", "indexKeyMap": {} }, + "entity": { "value": "%", "indexKeyMap": { "0": "Entity" } }, + "accessType": { "value": "argo-app", "indexKeyMap": {} } +}','now()','1','now()','1',true,false), + + ('apps','flux-app','view','View only', + 'Can view selected Flux CD application(s) and resource manifests of selected application(s)','{ + "role": { "value": "flux-app:view_%_%", + "indexKeyMap": { "14": "Env", "16": "App" } }, + "entityName": { "value": "%", "indexKeyMap": { "0": "App" } }, + "environment":{ "value": "%", "indexKeyMap": { "0": "Env" } }, + "action": { "value": "view", "indexKeyMap": {} }, + "entity": { "value": "%", "indexKeyMap": { "0": "Entity" } }, + "accessType": { "value": "flux-app", "indexKeyMap": {} } +}','now()','1','now()','1',true,false), + + ('apps','flux-app','admin','Admin', + 'Complete access on selected Flux CD application(s)','{ + "role": { "value": "flux-app:admin_%_%", + "indexKeyMap": { "15": "Env", "17": "App" } }, + "entityName": { "value": "%", "indexKeyMap": { "0": "App" } }, + "environment":{ "value": "%", "indexKeyMap": { "0": "Env" } }, + "action": { "value": "admin", "indexKeyMap": {} }, + "entity": { "value": "%", "indexKeyMap": { "0": "Entity" } }, + "accessType": { "value": "flux-app", "indexKeyMap": {} } +}','now()','1','now()','1',true,false); \ No newline at end of file diff --git a/util/rbac/EnforcerUtilGitOps.go b/util/rbac/EnforcerUtilGitOps.go new file mode 100644 index 0000000000..9b318a62bc --- /dev/null +++ b/util/rbac/EnforcerUtilGitOps.go @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rbac + +import ( + "fmt" + + "github.com/devtron-labs/devtron/pkg/cluster/repository" + "go.uber.org/zap" +) + +// EnforcerUtilGitOps builds RBAC objects for external Argo CD and Flux CD applications. +// +// The object is two segments — __/ — where the first segment +// is the environment_identifier convention used throughout Devtron. There is no project +// segment: external GitOps applications have no Devtron project, so unlike external Helm apps +// there is no team name and no "unassigned" placeholder to fill. +// +// Both segments are always populated, which matters because matchKeyByPart rejects an empty +// segment on either side unconditionally. +type EnforcerUtilGitOps interface { + // GetExternalGitOpsAppObject returns the RBAC object for an external Argo/Flux application. + // Returns an empty string if the cluster cannot be resolved, which callers must treat as + // a denial rather than as a wildcard. + GetExternalGitOpsAppObject(clusterId int, namespace string, appName string) string + // GetExternalGitOpsAppObjectByClusterName is the same, for callers that already hold the + // cluster name and can avoid the lookup. + GetExternalGitOpsAppObjectByClusterName(clusterName string, namespace string, appName string) string +} + +type EnforcerUtilGitOpsImpl struct { + logger *zap.SugaredLogger + clusterRepository repository.ClusterRepository +} + +func NewEnforcerUtilGitOpsImpl(logger *zap.SugaredLogger, + clusterRepository repository.ClusterRepository) *EnforcerUtilGitOpsImpl { + return &EnforcerUtilGitOpsImpl{ + logger: logger, + clusterRepository: clusterRepository, + } +} + +func (impl EnforcerUtilGitOpsImpl) GetExternalGitOpsAppObject(clusterId int, namespace string, appName string) string { + cluster, err := impl.clusterRepository.FindById(clusterId) + if err != nil { + impl.logger.Errorw("error on fetching cluster for rbac object", "err", err, "clusterId", clusterId) + return "" + } + return impl.GetExternalGitOpsAppObjectByClusterName(cluster.ClusterName, namespace, appName) +} + +func (impl EnforcerUtilGitOpsImpl) GetExternalGitOpsAppObjectByClusterName(clusterName string, namespace string, appName string) string { + if len(clusterName) == 0 || len(namespace) == 0 || len(appName) == 0 { + impl.logger.Errorw("incomplete identifier for rbac object, denying", + "clusterName", clusterName, "namespace", namespace, "appName", appName) + return "" + } + return fmt.Sprintf("%s__%s/%s", clusterName, namespace, appName) +} diff --git a/wire_gen.go b/wire_gen.go index e3518a51bb..5d29fcd5db 100644 --- a/wire_gen.go +++ b/wire_gen.go @@ -1009,7 +1009,8 @@ func InitializeApp() (*App, error) { coreAppRouterImpl := router.NewCoreAppRouterImpl(coreAppRestHandlerImpl) helmAppRestHandlerImpl := client3.NewHelmAppRestHandlerImpl(sugaredLogger, helmAppServiceImpl, enforcerImpl, clusterServiceImplExtended, enforcerUtilHelmImpl, appStoreDeploymentServiceImpl, installedAppDBServiceImpl, userServiceImpl, attributesServiceImpl, serverEnvConfigServerEnvConfig, fluxApplicationServiceImpl, argoApplicationServiceExtendedImpl) helmAppRouterImpl := client3.NewHelmAppRouterImpl(helmAppRestHandlerImpl) - k8sApplicationRestHandlerImpl := application3.NewK8sApplicationRestHandlerImpl(sugaredLogger, k8sApplicationServiceImpl, pumpImpl, terminalSessionHandlerImpl, enforcerImpl, enforcerUtilHelmImpl, enforcerUtilImpl, helmAppServiceImpl, userServiceImpl, k8sCommonServiceImpl, validate, environmentVariables, fluxApplicationServiceImpl, argoApplicationReadServiceImpl) + enforcerUtilGitOpsImpl := rbac.NewEnforcerUtilGitOpsImpl(sugaredLogger, clusterRepositoryImpl) + k8sApplicationRestHandlerImpl := application3.NewK8sApplicationRestHandlerImpl(sugaredLogger, k8sApplicationServiceImpl, pumpImpl, terminalSessionHandlerImpl, enforcerImpl, enforcerUtilHelmImpl, enforcerUtilGitOpsImpl, enforcerUtilImpl, helmAppServiceImpl, userServiceImpl, k8sCommonServiceImpl, validate, environmentVariables, fluxApplicationServiceImpl, argoApplicationReadServiceImpl) k8sApplicationRouterImpl := application3.NewK8sApplicationRouterImpl(k8sApplicationRestHandlerImpl) pProfRestHandlerImpl := restHandler.NewPProfRestHandler(userServiceImpl, enforcerImpl) pProfRouterImpl := router.NewPProfRouter(sugaredLogger, pProfRestHandlerImpl) @@ -1098,18 +1099,18 @@ func InitializeApp() (*App, error) { deploymentConfigurationRouterImpl := configDiff3.NewDeploymentConfigurationRouter(deploymentConfigurationRestHandlerImpl) infraConfigRestHandlerImpl := infraConfig.NewInfraConfigRestHandlerImpl(sugaredLogger, infraConfigServiceImpl, userServiceImpl, enforcerImpl, enforcerUtilImpl, validate) infraConfigRouterImpl := infraConfig.NewInfraProfileRouterImpl(infraConfigRestHandlerImpl) - argoApplicationRestHandlerImpl := argoApplication2.NewArgoApplicationRestHandlerImpl(argoApplicationServiceExtendedImpl, argoApplicationReadServiceImpl, sugaredLogger, enforcerImpl) + argoApplicationRestHandlerImpl := argoApplication2.NewArgoApplicationRestHandlerImpl(argoApplicationServiceExtendedImpl, argoApplicationReadServiceImpl, sugaredLogger, enforcerImpl, enforcerUtilGitOpsImpl) argoApplicationRouterImpl := argoApplication2.NewArgoApplicationRouterImpl(argoApplicationRestHandlerImpl) deploymentHistoryServiceImpl := cdPipeline.NewDeploymentHistoryServiceImpl(sugaredLogger, cdHandlerImpl, imageTaggingReadServiceImpl, imageTaggingServiceImpl, pipelineRepositoryImpl, deployedConfigurationHistoryServiceImpl) apiReqDecoderServiceImpl := devtronResource.NewAPIReqDecoderServiceImpl(sugaredLogger, pipelineRepositoryImpl) historyRestHandlerImpl := devtronResource2.NewHistoryRestHandlerImpl(sugaredLogger, enforcerImpl, deploymentHistoryServiceImpl, apiReqDecoderServiceImpl, enforcerUtilImpl) historyRouterImpl := devtronResource2.NewHistoryRouterImpl(historyRestHandlerImpl) devtronResourceRouterImpl := devtronResource2.NewDevtronResourceRouterImpl(historyRouterImpl) - fluxApplicationRestHandlerImpl := fluxApplication2.NewFluxApplicationRestHandlerImpl(fluxApplicationServiceImpl, sugaredLogger, enforcerImpl) + fluxApplicationRestHandlerImpl := fluxApplication2.NewFluxApplicationRestHandlerImpl(fluxApplicationServiceImpl, sugaredLogger, enforcerImpl, enforcerUtilGitOpsImpl) fluxApplicationRouterImpl := fluxApplication2.NewFluxApplicationRouterImpl(fluxApplicationRestHandlerImpl) scanningResultRestHandlerImpl := resourceScan.NewScanningResultRestHandlerImpl(sugaredLogger, userServiceImpl, imageScanServiceImpl, enforcerImpl, enforcerUtilImpl, validate) scanningResultRouterImpl := resourceScan.NewScanningResultRouterImpl(scanningResultRestHandlerImpl) - userResourceExtendedServiceImpl := userResource.NewUserResourceExtendedServiceImpl(sugaredLogger, teamServiceImpl, environmentServiceImpl, appCrudOperationServiceImpl, chartGroupServiceImpl, appListingServiceImpl, appWorkflowServiceImpl, k8sApplicationServiceImpl, clusterServiceImplExtended, commonEnforcementUtilImpl, enforcerUtilImpl, enforcerImpl) + userResourceExtendedServiceImpl := userResource.NewUserResourceExtendedServiceImpl(sugaredLogger, teamServiceImpl, environmentServiceImpl, appCrudOperationServiceImpl, chartGroupServiceImpl, appListingServiceImpl, appWorkflowServiceImpl, k8sApplicationServiceImpl, clusterServiceImplExtended, commonEnforcementUtilImpl, enforcerUtilImpl, enforcerImpl, argoApplicationServiceExtendedImpl, fluxApplicationServiceImpl) restHandlerImpl := userResource2.NewUserResourceRestHandler(sugaredLogger, userServiceImpl, userResourceExtendedServiceImpl) routerImpl := userResource2.NewUserResourceRouterImpl(restHandlerImpl) appManagementServiceImpl := overview.NewAppManagementServiceImpl(sugaredLogger, appRepositoryImpl, pipelineRepositoryImpl, ciPipelineRepositoryImpl, ciWorkflowRepositoryImpl, cdWorkflowRepositoryImpl, environmentRepositoryImpl, teamRepositoryImpl, workflowStageRepositoryImpl, repositoryImpl)