Skip to content

Commit bbb3ec7

Browse files
feat: RBAC support for external argo/flux apps (#7014)
* chore: create a makefile to setup and clean resources for development * feat: add flag for frontend to show argo/flux tabs - add migration files - append flags in the /orchestrator/user/check/roles API to show/hide argo/flux tabs * feat: changes for dropdown API in user permissions page * fix: missing dependencies in constructor method * fix: improper filtering logic - earlier filter was only applied with the cluster ID - add changes to filter based on cluster ID's and namespaces * fix: prevent connecting to kubelink for failed clusters * wip: policy enforcements * wip: policy enforcements * chore: remove test folder * fix: code review fixes * fix: code review fixes * fix: code review fixes * chore: rename migration scripts to maintain sync with enterprise * chore: change migration script number * fix: roles missing while fetching user roles * fix: deleted roles being listed * fix: deleted roles being listed * fix: review fixes
1 parent 72dcd49 commit bbb3ec7

28 files changed

Lines changed: 784 additions & 148 deletions

‎api/argoApplication/ArgoApplicationRestHandler.go‎

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ import (
2121
"errors"
2222
"github.com/devtron-labs/devtron/api/restHandler/common"
2323
"github.com/devtron-labs/devtron/pkg/argoApplication"
24+
"github.com/devtron-labs/devtron/pkg/argoApplication/bean"
2425
"github.com/devtron-labs/devtron/pkg/argoApplication/read"
2526
"github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin"
27+
"github.com/devtron-labs/devtron/util/rbac"
2628
"go.uber.org/zap"
2729
"net/http"
2830
"strconv"
@@ -39,26 +41,24 @@ type ArgoApplicationRestHandlerImpl struct {
3941
readService read.ArgoApplicationReadService
4042
logger *zap.SugaredLogger
4143
enforcer casbin.Enforcer
44+
enforcerUtilGitOps rbac.EnforcerUtilGitOps
4245
}
4346

4447
func NewArgoApplicationRestHandlerImpl(argoApplicationService argoApplication.ArgoApplicationService,
45-
readService read.ArgoApplicationReadService, logger *zap.SugaredLogger, enforcer casbin.Enforcer) *ArgoApplicationRestHandlerImpl {
48+
readService read.ArgoApplicationReadService, logger *zap.SugaredLogger, enforcer casbin.Enforcer,
49+
enforcerUtilGitOps rbac.EnforcerUtilGitOps) *ArgoApplicationRestHandlerImpl {
4650
return &ArgoApplicationRestHandlerImpl{
4751
argoApplicationService: argoApplicationService,
4852
readService: readService,
4953
logger: logger,
5054
enforcer: enforcer,
55+
enforcerUtilGitOps: enforcerUtilGitOps,
5156
}
5257

5358
}
5459

5560
func (handler *ArgoApplicationRestHandlerImpl) ListApplications(w http.ResponseWriter, r *http.Request) {
56-
// handle super-admin RBAC
5761
token := r.Header.Get("token")
58-
if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); !ok {
59-
common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden)
60-
return
61-
}
6262
v := r.URL.Query()
6363
clusterIdString := v.Get("clusterIds")
6464
var clusterIds []int
@@ -80,16 +80,34 @@ func (handler *ArgoApplicationRestHandlerImpl) ListApplications(w http.ResponseW
8080
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
8181
return
8282
}
83-
common.WriteJsonResp(w, nil, resp, http.StatusOK)
83+
// RBAC enforcer applying: filter the listing to the applications the caller may see.
84+
// Batched rather than a per-app Enforce loop; an app whose object cannot be built is dropped.
85+
objects := make([]string, 0, len(resp))
86+
objectByApp := make(map[*bean.ArgoApplicationListDto]string, len(resp))
87+
for _, app := range resp {
88+
object := handler.enforcerUtilGitOps.GetExternalGitOpsAppObjectByClusterName(app.ClusterName, app.Namespace, app.Name)
89+
if len(object) == 0 {
90+
continue
91+
}
92+
objectByApp[app] = object
93+
objects = append(objects, object)
94+
}
95+
authorisedObjects := make(map[string]bool)
96+
if len(objects) > 0 {
97+
authorisedObjects = handler.enforcer.EnforceInBatch(token, casbin.ResourceArgoApp, casbin.ActionGet, objects)
98+
}
99+
authorisedApps := make([]*bean.ArgoApplicationListDto, 0, len(resp))
100+
for _, app := range resp {
101+
if object, ok := objectByApp[app]; ok && authorisedObjects[strings.ToLower(object)] {
102+
authorisedApps = append(authorisedApps, app)
103+
}
104+
}
105+
//RBAC enforcer Ends
106+
common.WriteJsonResp(w, nil, authorisedApps, http.StatusOK)
84107
}
85108

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

@@ -108,6 +126,14 @@ func (handler *ArgoApplicationRestHandlerImpl) GetApplicationDetail(w http.Respo
108126
return
109127
}
110128
}
129+
// RBAC enforcer applying
130+
object := handler.enforcerUtilGitOps.GetExternalGitOpsAppObject(clusterId, namespace, resourceName)
131+
if len(object) == 0 || !handler.enforcer.Enforce(token, casbin.ResourceArgoApp, casbin.ActionGet, object) {
132+
common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden)
133+
return
134+
}
135+
//RBAC enforcer Ends
136+
111137
resp, err := handler.readService.GetAppDetailEA(ctx, resourceName, namespace, clusterId)
112138
if err != nil {
113139
handler.logger.Errorw("error in getting argo application app detail", "err", err, "resourceName", resourceName, "clusterId", clusterId)

‎api/auth/user/UserRestHandler.go‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ package user
1919
import (
2020
"encoding/json"
2121
"errors"
22+
"net/http"
23+
"strconv"
24+
"strings"
25+
2226
util2 "github.com/devtron-labs/devtron/api/auth/user/util"
2327
"github.com/devtron-labs/devtron/pkg/auth/user/helper"
2428
"github.com/devtron-labs/devtron/util/commonEnforcementFunctionsUtil"
2529
"github.com/gorilla/schema"
26-
"net/http"
27-
"strconv"
28-
"strings"
2930

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

42+
const (
43+
resultKeyArgoAppAccess = "hasArgoAppAccess"
44+
resultKeyFluxAppAccess = "hasFluxAppAccess"
45+
)
46+
47+
var gitOpsAccessResultKeyByResource = map[string]string{
48+
casbin.ResourceArgoApp: resultKeyArgoAppAccess,
49+
casbin.ResourceFluxApp: resultKeyFluxAppAccess,
50+
}
51+
4152
type UserRestHandler interface {
4253
CreateUser(w http.ResponseWriter, r *http.Request)
4354
UpdateUser(w http.ResponseWriter, r *http.Request)
@@ -795,9 +806,25 @@ func (handler UserRestHandlerImpl) CheckUserRoles(w http.ResponseWriter, r *http
795806
result := make(map[string]interface{})
796807
result["roles"] = roles
797808
result["superAdmin"] = false
809+
result[resultKeyArgoAppAccess] = false
810+
result[resultKeyFluxAppAccess] = false
798811
for _, item := range roles {
799812
if item == bean2.SUPERADMIN {
800813
result["superAdmin"] = true
814+
result[resultKeyArgoAppAccess] = true
815+
result[resultKeyFluxAppAccess] = true
816+
continue
817+
}
818+
819+
roleFragments := strings.Split(item, "_")
820+
resourceActionFragment := strings.Split(roleFragments[0], ":")
821+
822+
if len(resourceActionFragment) < 2 {
823+
continue
824+
}
825+
826+
if resultKey, ok := gitOpsAccessResultKeyByResource[resourceActionFragment[0]]; ok {
827+
result[resultKey] = true
801828
}
802829
}
803830
common.WriteJsonResp(w, err, result, http.StatusOK)

‎api/fluxApplication/FluxApplicationRestHandler.go‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"github.com/devtron-labs/devtron/api/restHandler/common"
66
"github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin"
77
clientErrors "github.com/devtron-labs/devtron/pkg/errors"
8+
"github.com/devtron-labs/devtron/util/rbac"
89
"github.com/devtron-labs/devtron/pkg/fluxApplication"
910
"github.com/gorilla/mux"
1011
"go.uber.org/zap"
@@ -20,26 +21,34 @@ type FluxApplicationRestHandlerImpl struct {
2021
fluxApplicationService fluxApplication.FluxApplicationService
2122
logger *zap.SugaredLogger
2223
enforcer casbin.Enforcer
24+
enforcerUtilGitOps rbac.EnforcerUtilGitOps
2325
}
2426

2527
func NewFluxApplicationRestHandlerImpl(fluxApplicationService fluxApplication.FluxApplicationService,
26-
logger *zap.SugaredLogger, enforcer casbin.Enforcer) *FluxApplicationRestHandlerImpl {
28+
logger *zap.SugaredLogger, enforcer casbin.Enforcer,
29+
enforcerUtilGitOps rbac.EnforcerUtilGitOps) *FluxApplicationRestHandlerImpl {
2730
return &FluxApplicationRestHandlerImpl{
2831
fluxApplicationService: fluxApplicationService,
2932
logger: logger,
3033
enforcer: enforcer,
34+
enforcerUtilGitOps: enforcerUtilGitOps,
3135
}
3236

3337
}
3438

39+
// checkFluxAppAuth builds the RBAC object from the app identity and enforces on it. Passed into
40+
// the service because the app list is streamed and cannot be filtered after the fact.
41+
func (handler *FluxApplicationRestHandlerImpl) checkFluxAppAuth(token string, clusterName string, namespace string, appName string) bool {
42+
object := handler.enforcerUtilGitOps.GetExternalGitOpsAppObjectByClusterName(clusterName, namespace, appName)
43+
if len(object) == 0 {
44+
return false
45+
}
46+
return handler.enforcer.Enforce(token, casbin.ResourceFluxApp, casbin.ActionGet, object)
47+
}
48+
3549
func (handler *FluxApplicationRestHandlerImpl) ListFluxApplications(w http.ResponseWriter, r *http.Request) {
3650

37-
//handle super-admin RBAC
3851
token := r.Header.Get("token")
39-
if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); !ok {
40-
common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden)
41-
return
42-
}
4352
v := r.URL.Query()
4453
clusterIdString := v.Get("clusterIds")
4554
var clusterIds []int
@@ -59,7 +68,7 @@ func (handler *FluxApplicationRestHandlerImpl) ListFluxApplications(w http.Respo
5968
return
6069
}
6170
handler.logger.Debugw("extracted ClusterIds successfully ", "clusterIds", clusterIds)
62-
handler.fluxApplicationService.ListFluxApplications(r.Context(), clusterIds, noStream, w)
71+
handler.fluxApplicationService.ListFluxApplications(r.Context(), clusterIds, noStream, w, token, handler.checkFluxAppAuth)
6372
}
6473

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

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

8697
res, err := handler.fluxApplicationService.GetFluxAppDetail(r.Context(), appIdentifier)
8798
if err != nil {

‎api/helm-app/wire_helmApp.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,7 @@ var HelmAppWireSet = wire.NewSet(
4141
gRPC.GetConfig,
4242
rbac.NewEnforcerUtilHelmImpl,
4343
wire.Bind(new(rbac.EnforcerUtilHelm), new(*rbac.EnforcerUtilHelmImpl)),
44+
45+
rbac.NewEnforcerUtilGitOpsImpl,
46+
wire.Bind(new(rbac.EnforcerUtilGitOps), new(*rbac.EnforcerUtilGitOpsImpl)),
4447
)

0 commit comments

Comments
 (0)