From 27e592fe40495cd5641e6e54bb8381517ab29b59 Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Fri, 27 Feb 2026 12:55:02 +0530 Subject: [PATCH 01/19] vendor files --- go.mod | 4 ++-- go.sum | 8 ++++---- vendor/modules.txt | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 2a2fcce947..f65514a229 100644 --- a/go.mod +++ b/go.mod @@ -338,7 +338,7 @@ require ( replace ( github.com/argoproj/argo-workflows/v3 v3.5.13 => github.com/devtron-labs/argo-workflows/v3 v3.5.13 github.com/cyphar/filepath-securejoin v0.4.1 => github.com/cyphar/filepath-securejoin v0.3.6 // indirect - github.com/devtron-labs/authenticator => github.com/devtron-labs/devtron-services/authenticator v0.0.0-20260224073514-1af84786f178 - github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20260224073514-1af84786f178 + github.com/devtron-labs/authenticator => github.com/devtron-labs/devtron-services/authenticator v0.0.0-20260227055702-7fe9d47354a0 + github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20260227055702-7fe9d47354a0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 => go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 ) diff --git a/go.sum b/go.sum index 37c4bc75fe..dd7b35158b 100644 --- a/go.sum +++ b/go.sum @@ -237,10 +237,10 @@ github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc h1:VRRKCwnzq github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/devtron-labs/argo-workflows/v3 v3.5.13 h1:3pINq0gXOSeTw2z/vYe+j80lRpSN5Rp/8mfQORh8SmU= github.com/devtron-labs/argo-workflows/v3 v3.5.13/go.mod h1:/vqxcovDPT4zqr4DjR5v7CF8ggpY1l3TSa2CIG3jmjA= -github.com/devtron-labs/devtron-services/authenticator v0.0.0-20260224073514-1af84786f178 h1:HeCudMPwqYZZwi8v1UybqhlfjoNoj0yCnG7aEm2fNFg= -github.com/devtron-labs/devtron-services/authenticator v0.0.0-20260224073514-1af84786f178/go.mod h1:9LCkYfiWaEKIBkmxw9jX1GujvEMyHwmDtVsatffAkeU= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20260224073514-1af84786f178 h1:TCMjY6m3RKEjbFfppYOgTpQvcHtuYAijzrCySyBM9WY= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20260224073514-1af84786f178/go.mod h1:d6awSGcXQc57s4PJlwcyACovJ4PgBmR9jZJ7h6CScUM= +github.com/devtron-labs/devtron-services/authenticator v0.0.0-20260227055702-7fe9d47354a0 h1:wrzPl8wa6ogrxpxS2I7J6ya65Z+U4rZTkzyfCyYWWYg= +github.com/devtron-labs/devtron-services/authenticator v0.0.0-20260227055702-7fe9d47354a0/go.mod h1:9LCkYfiWaEKIBkmxw9jX1GujvEMyHwmDtVsatffAkeU= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20260227055702-7fe9d47354a0 h1:KnmMU3qEy/QRtEJaUQNnXvCrintij+BYhOJtw+OjxYw= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20260227055702-7fe9d47354a0/go.mod h1:d6awSGcXQc57s4PJlwcyACovJ4PgBmR9jZJ7h6CScUM= github.com/devtron-labs/go-bitbucket v0.9.60-beta h1:VEx1jvDgdtDPS6A1uUFoaEi0l1/oLhbr+90xOwr6sDU= github.com/devtron-labs/go-bitbucket v0.9.60-beta/go.mod h1:GnuiCesvh8xyHeMCb+twm8lBR/kQzJYSKL28ZfObp1Y= github.com/devtron-labs/protos v0.0.3-0.20250323220609-ecf8a0f7305e h1:U6UdYbW8a7xn5IzFPd8cywjVVPfutGJCudjePAfL/Hs= diff --git a/vendor/modules.txt b/vendor/modules.txt index e9e5fdafff..4ee706189b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -523,7 +523,7 @@ github.com/davecgh/go-spew/spew # github.com/deckarep/golang-set v1.8.0 ## explicit; go 1.17 github.com/deckarep/golang-set -# github.com/devtron-labs/authenticator v0.4.35-0.20240809073103-6e11da8083f8 => github.com/devtron-labs/devtron-services/authenticator v0.0.0-20260224073514-1af84786f178 +# github.com/devtron-labs/authenticator v0.4.35-0.20240809073103-6e11da8083f8 => github.com/devtron-labs/devtron-services/authenticator v0.0.0-20260227055702-7fe9d47354a0 ## explicit; go 1.24.0 github.com/devtron-labs/authenticator/apiToken github.com/devtron-labs/authenticator/client @@ -531,7 +531,7 @@ github.com/devtron-labs/authenticator/jwt github.com/devtron-labs/authenticator/middleware github.com/devtron-labs/authenticator/oidc github.com/devtron-labs/authenticator/password -# github.com/devtron-labs/common-lib v0.18.1-0.20241001061923-eda545dc839e => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20260224073514-1af84786f178 +# github.com/devtron-labs/common-lib v0.18.1-0.20241001061923-eda545dc839e => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20260227055702-7fe9d47354a0 ## explicit; go 1.24.0 github.com/devtron-labs/common-lib/async github.com/devtron-labs/common-lib/blob-storage @@ -2675,5 +2675,5 @@ xorm.io/xorm/log xorm.io/xorm/names xorm.io/xorm/schemas xorm.io/xorm/tags -# github.com/devtron-labs/authenticator => github.com/devtron-labs/devtron-services/authenticator v0.0.0-20260224073514-1af84786f178 -# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20260224073514-1af84786f178 +# github.com/devtron-labs/authenticator => github.com/devtron-labs/devtron-services/authenticator v0.0.0-20260227055702-7fe9d47354a0 +# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20260227055702-7fe9d47354a0 From 37b07f15834ac1fae14aa9321eb404a33df93eee Mon Sep 17 00:00:00 2001 From: satya_prakash <155617493+SATYAsasini@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:50:13 +0530 Subject: [PATCH 02/19] feat: auto assign permission group (#6923) * feat: auto-assign role groups * fix: sync claims with casbin policy * feat: file re-structuring for ent oss sync * feat: global auth apis * sync file name with oss --- Wire.go | 4 +- .../GlobalConfigAuthorisationConfigRouter.go | 38 +++ .../GlobalConfigAuthorisationRestHandler.go | 143 ++++++++++ .../globalConfig/wire_globalConfig.go | 29 ++ api/auth/user/wire_selfRegistration.go | 1 + api/auth/user/wire_user.go | 11 + api/router/router.go | 7 + pkg/auth/authorisation/casbin/rbac.go | 75 ++++- .../GlobalAuthorisationConfigService.go | 256 ++++++++++++++++++ .../authorisation/globalConfig/bean/bean.go | 36 +++ .../GlobalAuthorisationConfigRepository.go | 152 +++++++++++ pkg/auth/sso/SSOLoginService.go | 50 +++- pkg/auth/user/UserSelfRegistrationService.go | 98 ++++--- pkg/auth/user/UserService.go | 47 ++-- pkg/auth/user/UserService_ent.go | 255 ++++++++++++++++- pkg/auth/user/adapter/adapter.go | 6 +- pkg/auth/user/bean/UserRequest.go | 19 +- .../UserAutoAssignGroupMapRepository.go | 112 ++++++++ pkg/auth/user/util/util.go | 10 + wire_gen.go | 165 +++++------ 20 files changed, 1342 insertions(+), 172 deletions(-) create mode 100644 api/auth/authorisation/globalConfig/GlobalConfigAuthorisationConfigRouter.go create mode 100644 api/auth/authorisation/globalConfig/GlobalConfigAuthorisationRestHandler.go create mode 100644 api/auth/authorisation/globalConfig/wire_globalConfig.go create mode 100644 pkg/auth/authorisation/globalConfig/GlobalAuthorisationConfigService.go create mode 100644 pkg/auth/authorisation/globalConfig/bean/bean.go create mode 100644 pkg/auth/authorisation/globalConfig/repository/GlobalAuthorisationConfigRepository.go create mode 100644 pkg/auth/user/repository/UserAutoAssignGroupMapRepository.go diff --git a/Wire.go b/Wire.go index 0ea78f820a..75264c635e 100644 --- a/Wire.go +++ b/Wire.go @@ -33,6 +33,7 @@ import ( appStoreDiscover "github.com/devtron-labs/devtron/api/appStore/discover" appStoreValues "github.com/devtron-labs/devtron/api/appStore/values" "github.com/devtron-labs/devtron/api/argoApplication" + "github.com/devtron-labs/devtron/api/auth/authorisation/globalConfig" "github.com/devtron-labs/devtron/api/auth/sso" "github.com/devtron-labs/devtron/api/auth/user" chartRepo "github.com/devtron-labs/devtron/api/chartRepo" @@ -192,6 +193,7 @@ func InitializeApp() (*App, error) { externalLink.ExternalLinkWireSet, team.TeamsWireSet, AuthWireSet, + globalConfig.GlobalConfigWireSet, util4.GetRuntimeConfig, util4.NewK8sUtil, wire.Bind(new(util4.K8sService), new(*util4.K8sServiceImpl)), @@ -993,7 +995,7 @@ func InitializeApp() (*App, error) { router.NewOverviewRouterImpl, wire.Bind(new(router.OverviewRouter), new(*router.OverviewRouterImpl)), - + restHandler.NewInfraOverviewRestHandlerImpl, wire.Bind(new(restHandler.InfraOverviewRestHandler), new(*restHandler.InfraOverviewRestHandlerImpl)), diff --git a/api/auth/authorisation/globalConfig/GlobalConfigAuthorisationConfigRouter.go b/api/auth/authorisation/globalConfig/GlobalConfigAuthorisationConfigRouter.go new file mode 100644 index 0000000000..a6a27ba413 --- /dev/null +++ b/api/auth/authorisation/globalConfig/GlobalConfigAuthorisationConfigRouter.go @@ -0,0 +1,38 @@ +/* + * 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 globalConfig + +import "github.com/gorilla/mux" + +type AuthorisationConfigRouter interface { + InitAuthorisationConfigRouter(router *mux.Router) +} + +type AuthorisationConfigRouterImpl struct { + handler AuthorisationConfigRestHandler +} + +func NewGlobalConfigAuthorisationRouterImpl(handler AuthorisationConfigRestHandler) *AuthorisationConfigRouterImpl { + return &AuthorisationConfigRouterImpl{handler: handler} +} + +func (router *AuthorisationConfigRouterImpl) InitAuthorisationConfigRouter(authorisationConfigRouter *mux.Router) { + authorisationConfigRouter.Path("/global-config"). + HandlerFunc(router.handler.CreateOrUpdateAuthorisationConfig).Methods("POST") + authorisationConfigRouter.Path("/global-config"). + HandlerFunc(router.handler.GetAllActiveAuthorisationConfig).Methods("GET") +} diff --git a/api/auth/authorisation/globalConfig/GlobalConfigAuthorisationRestHandler.go b/api/auth/authorisation/globalConfig/GlobalConfigAuthorisationRestHandler.go new file mode 100644 index 0000000000..7c3d48f838 --- /dev/null +++ b/api/auth/authorisation/globalConfig/GlobalConfigAuthorisationRestHandler.go @@ -0,0 +1,143 @@ +/* + * 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 globalConfig + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/devtron-labs/devtron/api/restHandler/common" + "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin" + auth "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig" + "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig/bean" + "github.com/devtron-labs/devtron/pkg/auth/user" + "github.com/devtron-labs/devtron/util/commonEnforcementFunctionsUtil" + "go.uber.org/zap" + "gopkg.in/go-playground/validator.v9" +) + +type AuthorisationConfigRestHandler interface { + CreateOrUpdateAuthorisationConfig(w http.ResponseWriter, r *http.Request) + GetAllActiveAuthorisationConfig(w http.ResponseWriter, r *http.Request) +} + +type AuthorisationConfigRestHandlerImpl struct { + validator *validator.Validate + logger *zap.SugaredLogger + enforcer casbin.Enforcer + userService user.UserService + userCommonService user.UserCommonService + globalAuthorisationConfigService auth.GlobalAuthorisationConfigService + rbacEnforcementUtil commonEnforcementFunctionsUtil.CommonEnforcementUtil +} + +func NewGlobalAuthorisationConfigRestHandlerImpl(validator *validator.Validate, + logger *zap.SugaredLogger, enforcer casbin.Enforcer, + userService user.UserService, + globalAuthorisationConfigService auth.GlobalAuthorisationConfigService, + userCommonService user.UserCommonService, + rbacEnforcementUtil commonEnforcementFunctionsUtil.CommonEnforcementUtil, +) *AuthorisationConfigRestHandlerImpl { + return &AuthorisationConfigRestHandlerImpl{ + validator: validator, + logger: logger, + enforcer: enforcer, + userService: userService, + globalAuthorisationConfigService: globalAuthorisationConfigService, + userCommonService: userCommonService, + rbacEnforcementUtil: rbacEnforcementUtil, + } +} + +func (handler *AuthorisationConfigRestHandlerImpl) CreateOrUpdateAuthorisationConfig(w http.ResponseWriter, r *http.Request) { + userId, err := handler.userService.GetLoggedInUser(r) + if userId == 0 || err != nil { + common.HandleUnauthorized(w, r) + return + } + decoder := json.NewDecoder(r.Body) + var globalConfigPayload bean.GlobalAuthorisationConfig + err = decoder.Decode(&globalConfigPayload) + if err != nil { + handler.logger.Errorw("request err, CreateOrUpdateAuthorisationConfig", "err", err, "payload", globalConfigPayload) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + token := r.Header.Get("token") + if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionCreate, "*"); !ok { + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + return + } + isValidationError, err := handler.validateGlobalAuthorisationConfigPayload(globalConfigPayload) + if err != nil { + handler.logger.Errorw("error, validateGlobalAuthorisationConfigPayload", "payload", globalConfigPayload, "err", err) + if isValidationError { + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + globalConfigPayload.UserId = userId + resp, err := handler.globalAuthorisationConfigService.CreateOrUpdateGlobalAuthConfig(globalConfigPayload, nil) + if err != nil { + handler.logger.Errorw("service error, CreateOrUpdateAuthorisationConfig", "err", err, "payload", globalConfigPayload) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + common.WriteJsonResp(w, nil, resp, http.StatusOK) +} + +func (handler *AuthorisationConfigRestHandlerImpl) GetAllActiveAuthorisationConfig(w http.ResponseWriter, r *http.Request) { + userId, err := handler.userService.GetLoggedInUser(r) + if userId == 0 || err != nil { + common.HandleUnauthorized(w, r) + return + } + token := r.Header.Get("token") + isAuthorised, err := handler.rbacEnforcementUtil.CheckRbacForMangerAndAboveAccess(token, userId) + if err != nil { + handler.logger.Errorw("error, GetAllActiveAuthorisationConfig", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + if !isAuthorised { + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + return + } + resp, err := handler.globalAuthorisationConfigService.GetAllActiveAuthorisationConfig() + if err != nil { + handler.logger.Errorw("service error, GetAllActiveAuthorisationConfig", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + common.WriteJsonResp(w, nil, resp, http.StatusOK) +} + +func (handler *AuthorisationConfigRestHandlerImpl) validateGlobalAuthorisationConfigPayload(globalConfigPayload bean.GlobalAuthorisationConfig) (bool, error) { + err := handler.validator.Struct(globalConfigPayload) + if err != nil { + handler.logger.Errorw("err, validateGlobalAuthorisationConfigPayload", "payload", globalConfigPayload, "err", err) + return true, err + } + if len(globalConfigPayload.ConfigTypes) == 0 { + handler.logger.Errorw("err, validation failed on validateGlobalAuthorisationConfigPayload due to no configType provided", "payload", globalConfigPayload) + return true, errors.New("no configTypes provided in request") + } + return false, nil +} diff --git a/api/auth/authorisation/globalConfig/wire_globalConfig.go b/api/auth/authorisation/globalConfig/wire_globalConfig.go new file mode 100644 index 0000000000..44b4efd523 --- /dev/null +++ b/api/auth/authorisation/globalConfig/wire_globalConfig.go @@ -0,0 +1,29 @@ +/* + * 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 globalConfig + +import "github.com/google/wire" + +// GlobalConfigWireSet wires the REST handler and router for the authorisation global-config API. +// NOTE: GlobalAuthorisationConfigRepository and GlobalAuthorisationConfigService are already +// provided by UserWireSet (api/auth/user/wire_user.go) and must not be re-declared here. +var GlobalConfigWireSet = wire.NewSet( + NewGlobalAuthorisationConfigRestHandlerImpl, + wire.Bind(new(AuthorisationConfigRestHandler), new(*AuthorisationConfigRestHandlerImpl)), + NewGlobalConfigAuthorisationRouterImpl, + wire.Bind(new(AuthorisationConfigRouter), new(*AuthorisationConfigRouterImpl)), +) diff --git a/api/auth/user/wire_selfRegistration.go b/api/auth/user/wire_selfRegistration.go index e383b2b733..9e8d62769e 100644 --- a/api/auth/user/wire_selfRegistration.go +++ b/api/auth/user/wire_selfRegistration.go @@ -25,6 +25,7 @@ import ( //depends on sql, //TODO integrate user auth module +// SelfRegistrationWireSet depends on GlobalAuthorisationConfigService which is provided by UserWireSet var SelfRegistrationWireSet = wire.NewSet( repository.NewSelfRegistrationRolesRepositoryImpl, wire.Bind(new(repository.SelfRegistrationRolesRepository), new(*repository.SelfRegistrationRolesRepositoryImpl)), diff --git a/api/auth/user/wire_user.go b/api/auth/user/wire_user.go index 6c421698d5..b4a923a7be 100644 --- a/api/auth/user/wire_user.go +++ b/api/auth/user/wire_user.go @@ -19,6 +19,8 @@ package user import ( "github.com/devtron-labs/devtron/pkg/auth/authentication" "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin" + globalConfig "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig" + globalConfigRepo "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig/repository" user2 "github.com/devtron-labs/devtron/pkg/auth/user" repository2 "github.com/devtron-labs/devtron/pkg/auth/user/repository" "github.com/google/wire" @@ -77,4 +79,13 @@ var UserWireSet = wire.NewSet( wire.Bind(new(RbacRoleRestHandler), new(*RbacRoleRestHandlerImpl)), user2.NewRbacRoleServiceImpl, wire.Bind(new(user2.RbacRoleService), new(*user2.RbacRoleServiceImpl)), + + repository2.NewUserAutoAssignGroupMapRepositoryImpl, + wire.Bind(new(repository2.UserAutoAssignGroupMapRepository), new(*repository2.UserAutoAssignGroupMapRepositoryImpl)), + + globalConfigRepo.NewGlobalAuthorisationConfigRepositoryImpl, + wire.Bind(new(globalConfigRepo.GlobalAuthorisationConfigRepository), new(*globalConfigRepo.GlobalAuthorisationConfigRepositoryImpl)), + + globalConfig.NewGlobalAuthorisationConfigServiceImpl, + wire.Bind(new(globalConfig.GlobalAuthorisationConfigService), new(*globalConfig.GlobalAuthorisationConfigServiceImpl)), ) diff --git a/api/router/router.go b/api/router/router.go index a1a7a05662..df240ee8ec 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -25,6 +25,7 @@ import ( "github.com/devtron-labs/devtron/api/appStore/chartGroup" appStoreDeployment "github.com/devtron-labs/devtron/api/appStore/deployment" "github.com/devtron-labs/devtron/api/argoApplication" + "github.com/devtron-labs/devtron/api/auth/authorisation/globalConfig" "github.com/devtron-labs/devtron/api/auth/sso" "github.com/devtron-labs/devtron/api/auth/user" "github.com/devtron-labs/devtron/api/chartRepo" @@ -126,6 +127,7 @@ type MuxRouter struct { scanningResultRouter resourceScan.ScanningResultRouter userResourceRouter userResource.Router overviewRouter OverviewRouter + globalAuthorisationConfigRouter globalConfig.AuthorisationConfigRouter } func NewMuxRouter(logger *zap.SugaredLogger, @@ -162,6 +164,7 @@ func NewMuxRouter(logger *zap.SugaredLogger, scanningResultRouter resourceScan.ScanningResultRouter, userResourceRouter userResource.Router, overviewRouter OverviewRouter, + globalAuthorisationConfigRouter globalConfig.AuthorisationConfigRouter, ) *MuxRouter { r := &MuxRouter{ Router: mux.NewRouter(), @@ -230,6 +233,7 @@ func NewMuxRouter(logger *zap.SugaredLogger, scanningResultRouter: scanningResultRouter, userResourceRouter: userResourceRouter, overviewRouter: overviewRouter, + globalAuthorisationConfigRouter: globalAuthorisationConfigRouter, } return r } @@ -306,6 +310,9 @@ func (r MuxRouter) Init() { userRouter := r.Router.PathPrefix("/orchestrator/user").Subrouter() r.UserRouter.InitUserRouter(userRouter) + authorisationConfigRouter := r.Router.PathPrefix("/orchestrator/authorisation").Subrouter() + r.globalAuthorisationConfigRouter.InitAuthorisationConfigRouter(authorisationConfigRouter) + chartRefRouter := r.Router.PathPrefix("/orchestrator/chartref").Subrouter() r.ChartRefRouter.initChartRefRouter(chartRefRouter) diff --git a/pkg/auth/authorisation/casbin/rbac.go b/pkg/auth/authorisation/casbin/rbac.go index 1782602ae0..51df9c694c 100644 --- a/pkg/auth/authorisation/casbin/rbac.go +++ b/pkg/auth/authorisation/casbin/rbac.go @@ -31,6 +31,8 @@ import ( "github.com/casbin/casbin" "github.com/devtron-labs/authenticator/jwt" "github.com/devtron-labs/authenticator/middleware" + globalConfig "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig" + util3 "github.com/devtron-labs/devtron/pkg/auth/user/util" "github.com/patrickmn/go-cache" "go.uber.org/zap" "google.golang.org/grpc/codes" @@ -53,7 +55,8 @@ func NewEnforcerImpl( enforcer *casbin.SyncedEnforcer, enforcerV2 *casbinv2.SyncedEnforcer, sessionManager *middleware.SessionManager, - logger *zap.SugaredLogger) (*EnforcerImpl, error) { + logger *zap.SugaredLogger, + globalAuthorisationConfigService globalConfig.GlobalAuthorisationConfigService) (*EnforcerImpl, error) { lock := make(map[string]*CacheData) batchRequestLock := make(map[string]*sync.Mutex) enforcerConfig, err := getConfig() @@ -61,7 +64,8 @@ func NewEnforcerImpl( return nil, err } enf := &EnforcerImpl{lockCacheData: lock, enforcerRWLock: &sync.RWMutex{}, batchRequestLock: batchRequestLock, enforcerConfig: enforcerConfig, - Cache: getEnforcerCache(logger, enforcerConfig), Enforcer: enforcer, EnforcerV2: enforcerV2, logger: logger, SessionManager: sessionManager} + Cache: getEnforcerCache(logger, enforcerConfig), Enforcer: enforcer, EnforcerV2: enforcerV2, logger: logger, SessionManager: sessionManager, + globalAuthorisationConfigService: globalAuthorisationConfigService} setEnforcerImpl(enf) return enf, nil } @@ -113,9 +117,10 @@ type EnforcerImpl struct { Enforcer *casbin.SyncedEnforcer EnforcerV2 *casbinv2.SyncedEnforcer *middleware.SessionManager - logger *zap.SugaredLogger - enforcerConfig *EnforcerConfig - enforcerRWLock *sync.RWMutex + logger *zap.SugaredLogger + enforcerConfig *EnforcerConfig + enforcerRWLock *sync.RWMutex + globalAuthorisationConfigService globalConfig.GlobalAuthorisationConfigService } // Enforce is a wrapper around casbin.Enforce to additionally enforce a default role and a custom @@ -392,21 +397,38 @@ func (e *EnforcerImpl) GetCacheDump() string { // enforce is a helper to additionally check a default role and invoke a custom claims enforcement function func (e *EnforcerImpl) enforce(token string, resource string, action string, resourceItem string) bool { - // check the default role - email, invalid := e.VerifyTokenAndGetEmail(token) + subjects, invalid := e.getSubjectsFromToken(token) if invalid { return false } - return e.EnforceByEmail(email, resource, action, resourceItem) + for _, subject := range subjects { + if e.EnforceByEmail(subject, resource, action, resourceItem) { + return true + } + } + return false } // enforceInBatch is a helper to additionally check a default role and invoke a custom claims enforcement function func (e *EnforcerImpl) enforceInBatch(token string, resource string, action string, vals []string) map[string]bool { - email, invalid := e.VerifyTokenAndGetEmail(token) + subjects, invalid := e.getSubjectsFromToken(token) if invalid { return make(map[string]bool) } - return e.EnforceByEmailInBatch(email, resource, action, vals) + if len(subjects) == 1 { + return e.EnforceByEmailInBatch(subjects[0], resource, action, vals) + } + // multiple subjects (group claims active): any subject allowed = allowed + result := make(map[string]bool) + for _, subject := range subjects { + subjectResult := e.EnforceByEmailInBatch(subject, resource, action, vals) + for k, v := range subjectResult { + if v { + result[k] = true + } + } + } + return result } func (e *EnforcerImpl) enforceAndUpdateCache(email string, resource string, action string, resourceItem string) bool { @@ -467,6 +489,39 @@ func (e *EnforcerImpl) VerifyTokenAndGetEmail(tokenString string) (string, bool) return email, false } +// getSubjectsFromToken parses the JWT token and returns all casbin subjects for the user. +// When group claims config is active, returns [email, group:casbin1, group:casbin2, ...]. +// Otherwise returns just [email]. The invalid bool is true if the token is invalid. +func (e *EnforcerImpl) getSubjectsFromToken(tokenString string) ([]string, bool) { + claims, err := e.SessionManager.VerifyToken(tokenString) + if err != nil { + return nil, true + } + mapClaims, err := jwt.MapClaims(claims) + if err != nil { + return nil, true + } + email := jwt.GetField(mapClaims, "email") + sub := jwt.GetField(mapClaims, "sub") + if email == "" && (sub == "admin" || sub == "admin:login") { + email = "admin" + } + if email == "" { + return nil, true + } + subjects := []string{email} + if e.globalAuthorisationConfigService != nil && + e.globalAuthorisationConfigService.IsGroupClaimsConfigActive() && + !util3.CheckIfAdminOrApiToken(email) { + _, groups := e.globalAuthorisationConfigService.GetEmailAndGroupsFromClaims(mapClaims) + if len(groups) > 0 { + groupCasbinNames := util3.GetGroupCasbinName(groups) + subjects = append(subjects, groupCasbinNames...) + } + } + return subjects, false +} + // enforce is a helper to additionally check a default role and invoke a custom claims enforcement function func (e *EnforcerImpl) enforceByEmail(emailId string, resource string, action string, resourceItem string) bool { defer handlePanic() diff --git a/pkg/auth/authorisation/globalConfig/GlobalAuthorisationConfigService.go b/pkg/auth/authorisation/globalConfig/GlobalAuthorisationConfigService.go new file mode 100644 index 0000000000..529fe2a39c --- /dev/null +++ b/pkg/auth/authorisation/globalConfig/GlobalAuthorisationConfigService.go @@ -0,0 +1,256 @@ +/* + * 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 globalConfig + +import ( + "github.com/devtron-labs/authenticator/jwt" + "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig/bean" + "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig/repository" + util3 "github.com/devtron-labs/devtron/pkg/auth/user/util" + "github.com/go-pg/pg" + jwtv4 "github.com/golang-jwt/jwt/v4" + "go.uber.org/zap" + "strings" + "time" +) + +type GlobalAuthorisationConfigService interface { + GetCacheDataForActiveConfigs() (map[string]bool, error) + ReloadCache() + CreateOrUpdateGlobalAuthConfig(globalAuthConfig bean.GlobalAuthorisationConfig, tx *pg.Tx) ([]*bean.GlobalAuthorisationConfigResponse, error) + CreateOrUpdateGroupClaimsAuthConfig(tx *pg.Tx, authConfigType string, userId int32) ([]*bean.GlobalAuthorisationConfigResponse, error) + GetAllActiveAuthorisationConfig() ([]*bean.GlobalAuthorisationConfigResponse, error) + IsGroupClaimsConfigActive() bool + IsDevtronSystemManagedConfigActive() bool + GetEmailAndGroupsFromClaims(claims jwtv4.MapClaims) (string, []string) +} + +type GlobalAuthorisationConfigServiceImpl struct { + logger *zap.SugaredLogger + globalAuthorisationConfigRepository repository.GlobalAuthorisationConfigRepository + globalAuthActiveConfigCache map[string]bool +} + +func NewGlobalAuthorisationConfigServiceImpl(logger *zap.SugaredLogger, + globalAuthorisationConfigRepository repository.GlobalAuthorisationConfigRepository) *GlobalAuthorisationConfigServiceImpl { + globalAuthorisationConfigImpl := &GlobalAuthorisationConfigServiceImpl{ + logger: logger, + globalAuthorisationConfigRepository: globalAuthorisationConfigRepository, + } + activeConfigCache, err := globalAuthorisationConfigImpl.GetCacheDataForActiveConfigs() + if err != nil { + globalAuthorisationConfigImpl.logger.Errorw("error in caching on start up", "err", err) + } + //Setting cache for active configs, will be set to empty map when error is caught + globalAuthorisationConfigImpl.globalAuthActiveConfigCache = activeConfigCache + + return globalAuthorisationConfigImpl +} + +// GetCacheDataForActiveConfigs Caches the active global authorisation config +func (impl *GlobalAuthorisationConfigServiceImpl) GetCacheDataForActiveConfigs() (map[string]bool, error) { + activeConfigMap := make(map[string]bool) + activeConfigs, err := impl.globalAuthorisationConfigRepository.GetAllActiveConfigs() + if err != nil { + impl.logger.Errorw("error in getting all active configs for cache", "err", "err") + return activeConfigMap, err + } + for _, config := range activeConfigs { + activeConfigMap[config.ConfigType] = config.Active + } + return activeConfigMap, err +} + +// ReloadCache updates the cache and does not updates cache if any error is caught +func (impl *GlobalAuthorisationConfigServiceImpl) ReloadCache() { + configCache, err := impl.GetCacheDataForActiveConfigs() + if err != nil { + impl.logger.Errorw("cache can not be reloaded, using previous cache", "err", err) + return + } + //Setting new cache + impl.globalAuthActiveConfigCache = configCache +} + +func (impl *GlobalAuthorisationConfigServiceImpl) CreateOrUpdateGroupClaimsAuthConfig(tx *pg.Tx, authConfigType string, userId int32) ([]*bean.GlobalAuthorisationConfigResponse, error) { + configType := []string{authConfigType} + authConfig := bean.GlobalAuthorisationConfig{ + ConfigTypes: configType, + UserId: userId, + } + resp, err := impl.CreateOrUpdateGlobalAuthConfig(authConfig, tx) + if err != nil { + impl.logger.Errorw("error in CreateOrUpdateGroupClaimsAuthConfig", "err", err) + return nil, err + } + return resp, nil +} + +func (impl *GlobalAuthorisationConfigServiceImpl) CreateOrUpdateGlobalAuthConfig(globalAuthConfig bean.GlobalAuthorisationConfig, tx *pg.Tx) ([]*bean.GlobalAuthorisationConfigResponse, error) { + configs, err := impl.globalAuthorisationConfigRepository.GetByConfigTypes(globalAuthConfig.ConfigTypes) + if err != nil { + impl.logger.Errorw("error in checking global auth config exist by config type", "err", err, "configTypes", globalAuthConfig.ConfigTypes) + return nil, err + } + existingConfigMap := make(map[string]*repository.GlobalAuthorisationConfig) + for _, config := range configs { + if len(config.ConfigType) > 0 && config.Id > 0 { + existingConfigMap[config.ConfigType] = config + } + } + createConfigModels := make([]*repository.GlobalAuthorisationConfig, 0, len(globalAuthConfig.ConfigTypes)) + updateConfigModels := make([]*repository.GlobalAuthorisationConfig, 0, len(globalAuthConfig.ConfigTypes)) + for _, cfg := range globalAuthConfig.ConfigTypes { + if cfgModel, ok := existingConfigMap[cfg]; !ok { + // Create config model for bulk operations + configModel := impl.createConfigModel(globalAuthConfig.UserId, cfg) + createConfigModels = append(createConfigModels, configModel) + } else { + // update to active model for bulk operations + updateConfigModels = append(updateConfigModels, impl.updateConfigModel(cfgModel, globalAuthConfig.UserId)) + } + } + //Checking if transaction exists, if not make a new transaction + isNewTransaction := false + if tx == nil { + tx, err = impl.globalAuthorisationConfigRepository.StartATransaction() + if err != nil { + impl.logger.Errorw("error in starting a transaction", "err", err) + return nil, err + } + isNewTransaction = true + // Rollback tx on error. + defer tx.Rollback() + } + if len(createConfigModels) > 0 { + err = impl.globalAuthorisationConfigRepository.CreateConfig(tx, createConfigModels) + if err != nil { + impl.logger.Errorw("error in creating configs in bulk", "err", err, "createConfigModels", createConfigModels) + return nil, err + } + } + if len(updateConfigModels) > 0 { + err = impl.globalAuthorisationConfigRepository.UpdateConfig(tx, updateConfigModels) + if err != nil { + impl.logger.Errorw("error in updating configs in bulk", "err", err, "updateConfigModels", updateConfigModels) + return nil, err + } + } + + // Updating all configs to inactive except those got in request(transactional) + err = impl.globalAuthorisationConfigRepository.SetConfigsToInactiveExceptGivenConfigs(tx, globalAuthConfig.ConfigTypes) + if err != nil { + impl.logger.Errorw("error in SetConfigsToInactiveExceptGivenConfigs", "err", err, "configTypes", globalAuthConfig.ConfigTypes) + return nil, err + } + + if isNewTransaction { + err = impl.globalAuthorisationConfigRepository.CommitATransaction(tx) + if err != nil { + impl.logger.Errorw("error in committing a transaction", "err", err) + return nil, err + } + } + allConfigModel := append(createConfigModels, updateConfigModels...) + globalConfigResponse := make([]*bean.GlobalAuthorisationConfigResponse, 0, len(allConfigModel)) + for _, authConfig := range allConfigModel { + config := &bean.GlobalAuthorisationConfigResponse{ + Id: authConfig.Id, + ConfigType: authConfig.ConfigType, + Active: authConfig.Active, + } + globalConfigResponse = append(globalConfigResponse, config) + } + + // Reloading Local Cache on Save/Update + impl.ReloadCache() + return globalConfigResponse, err +} + +func (impl *GlobalAuthorisationConfigServiceImpl) createConfigModel(userId int32, configType string) *repository.GlobalAuthorisationConfig { + configModel := &repository.GlobalAuthorisationConfig{} + configModel.ConfigType = configType + configModel.Active = true + configModel.CreatedOn = time.Now() + configModel.CreatedBy = userId + configModel.UpdatedOn = time.Now() + configModel.UpdatedBy = userId + + return configModel +} + +func (impl *GlobalAuthorisationConfigServiceImpl) updateConfigModel(configModel *repository.GlobalAuthorisationConfig, userId int32) *repository.GlobalAuthorisationConfig { + configModel.Active = true + configModel.UpdatedOn = time.Now() + configModel.UpdatedBy = userId + return configModel +} + +func (impl *GlobalAuthorisationConfigServiceImpl) GetAllActiveAuthorisationConfig() ([]*bean.GlobalAuthorisationConfigResponse, error) { + configs, err := impl.globalAuthorisationConfigRepository.GetAllActiveConfigs() + if err != nil { + impl.logger.Errorw("error in getting authorisation config by config type ", "err", err) + return nil, err + } + var authConfigsResponse []*bean.GlobalAuthorisationConfigResponse + for _, config := range configs { + authConfig := &bean.GlobalAuthorisationConfigResponse{ + Id: config.Id, + ConfigType: config.ConfigType, + Active: config.Active, + } + authConfigsResponse = append(authConfigsResponse, authConfig) + } + + return authConfigsResponse, nil +} + +func (impl *GlobalAuthorisationConfigServiceImpl) GetEmailAndGroupsFromClaims(claims jwtv4.MapClaims) (string, []string) { + email := jwt.GetField(claims, "email") + sub := jwt.GetField(claims, "sub") + if email == "" && (sub == "admin" || sub == "admin:login") { + email = "admin" + } + email = util3.ConvertEmailToLowerCase(email) + groups := make([]string, 0) + groupClaimsIf := jwt.GetFieldInterface(claims, "groups") + if groupClaimsArr, ok := groupClaimsIf.([]interface{}); ok { + for _, groupClaim := range groupClaimsArr { + if group, ok := groupClaim.(string); ok { + groups = append(groups, group) + } + } + } else if groupClaimStr, ok := groupClaimsIf.(string); ok { + splitGroups := strings.Split(groupClaimStr, ",") + groups = splitGroups + } + return email, groups +} + +func (impl *GlobalAuthorisationConfigServiceImpl) IsGroupClaimsConfigActive() bool { + if _, ok := impl.globalAuthActiveConfigCache[string(bean.GroupClaims)]; ok { + return true + } + return false +} + +func (impl *GlobalAuthorisationConfigServiceImpl) IsDevtronSystemManagedConfigActive() bool { + if _, ok := impl.globalAuthActiveConfigCache[string(bean.DevtronSystemManaged)]; ok { + return true + } + return false +} diff --git a/pkg/auth/authorisation/globalConfig/bean/bean.go b/pkg/auth/authorisation/globalConfig/bean/bean.go new file mode 100644 index 0000000000..38682af676 --- /dev/null +++ b/pkg/auth/authorisation/globalConfig/bean/bean.go @@ -0,0 +1,36 @@ +/* + * 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 bean + +type GlobalAuthorisationConfigType string + +const ( + DevtronSystemManaged GlobalAuthorisationConfigType = "devtron-system-managed" + DevtronSelfRegisteredGroup GlobalAuthorisationConfigType = "devtron-self-registered-group" + GroupClaims GlobalAuthorisationConfigType = "group-claims" // GroupClaims are currently used for Active directory and LDAP +) + +type GlobalAuthorisationConfig struct { + ConfigTypes []string `json:"configTypes" validate:"required"` + UserId int32 `json:"userId"` //for Internal Use +} + +type GlobalAuthorisationConfigResponse struct { + Id int `json:"id"` + ConfigType string `json:"configType"` + Active bool `json:"active"` +} diff --git a/pkg/auth/authorisation/globalConfig/repository/GlobalAuthorisationConfigRepository.go b/pkg/auth/authorisation/globalConfig/repository/GlobalAuthorisationConfigRepository.go new file mode 100644 index 0000000000..7ba82f7c25 --- /dev/null +++ b/pkg/auth/authorisation/globalConfig/repository/GlobalAuthorisationConfigRepository.go @@ -0,0 +1,152 @@ +/* + * 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 repository + +import ( + "github.com/devtron-labs/devtron/pkg/sql" + "github.com/go-pg/pg" + "go.uber.org/zap" +) + +type GlobalAuthorisationConfigRepository interface { + StartATransaction() (*pg.Tx, error) + CommitATransaction(tx *pg.Tx) error + GetAllActiveConfigs() ([]*GlobalAuthorisationConfig, error) + SetConfigToInactiveByConfigType(configType string) error + GetByConfigType(configType string) (*GlobalAuthorisationConfig, error) + GetByConfigTypes(configType []string) ([]*GlobalAuthorisationConfig, error) + CreateConfig(tx *pg.Tx, model []*GlobalAuthorisationConfig) error + UpdateConfig(tx *pg.Tx, model []*GlobalAuthorisationConfig) error + SetConfigsToInactiveExceptGivenConfigs(tx *pg.Tx, configTypes []string) error +} + +type GlobalAuthorisationConfigRepositoryImpl struct { + logger *zap.SugaredLogger + dbConnection *pg.DB +} + +type GlobalAuthorisationConfig struct { + TableName struct{} `sql:"global_authorisation_config" pg:",discard_unknown_columns"` + Id int `sql:"id,pk"` + ConfigType string `sql:"config_type,notnull"` + Active bool `sql:"active,notnull"` + sql.AuditLog +} + +func NewGlobalAuthorisationConfigRepositoryImpl(logger *zap.SugaredLogger, + dbConnection *pg.DB) *GlobalAuthorisationConfigRepositoryImpl { + globalAuthorisationConfigRepositoryImpl := &GlobalAuthorisationConfigRepositoryImpl{ + logger: logger, + dbConnection: dbConnection, + } + return globalAuthorisationConfigRepositoryImpl +} + +func (repo *GlobalAuthorisationConfigRepositoryImpl) StartATransaction() (*pg.Tx, error) { + tx, err := repo.dbConnection.Begin() + if err != nil { + repo.logger.Errorw("error in beginning a transaction", "err", err) + return nil, err + } + return tx, nil +} + +func (repo *GlobalAuthorisationConfigRepositoryImpl) CommitATransaction(tx *pg.Tx) error { + err := tx.Commit() + if err != nil { + repo.logger.Errorw("error in commiting a transaction", "err", err) + return err + } + return nil +} + +func (repo *GlobalAuthorisationConfigRepositoryImpl) GetAllActiveConfigs() ([]*GlobalAuthorisationConfig, error) { + var models []*GlobalAuthorisationConfig + err := repo.dbConnection.Model(&models).Where("active = ?", true).Select() + if err != nil { + repo.logger.Errorw("error in getting all active config from global authorisation config", "err", err) + return models, err + } + return models, err +} + +func (repo *GlobalAuthorisationConfigRepositoryImpl) SetConfigToInactiveByConfigType(configType string) error { + var model *GlobalAuthorisationConfig + _, err := repo.dbConnection.Model(&model).Set("active = ?", false). + Where("active = ?", true). + Where("config_type = ?", configType). + Update() + if err != nil { + repo.logger.Errorw("error in setting config type to inactive", "err", err, "configType", configType) + return err + } + return nil +} + +func (repo *GlobalAuthorisationConfigRepositoryImpl) GetByConfigType(configType string) (*GlobalAuthorisationConfig, error) { + var model *GlobalAuthorisationConfig + err := repo.dbConnection.Model(&model). + Where("config_type= ? ", configType). + Where("active = ?", true). + Select() + if err != nil { + repo.logger.Errorw("error in getting config by configType", "err", err, "configType", configType) + return nil, err + } + return model, err +} + +func (repo *GlobalAuthorisationConfigRepositoryImpl) GetByConfigTypes(configTypes []string) ([]*GlobalAuthorisationConfig, error) { + var model []*GlobalAuthorisationConfig + err := repo.dbConnection.Model(&model). + Where("config_type in (?) ", pg.In(configTypes)). + Select() + if err != nil { + repo.logger.Errorw("error in getting configs by configTypes", "err", err, "configTypes", configTypes) + return nil, err + } + return model, err +} + +func (repo *GlobalAuthorisationConfigRepositoryImpl) CreateConfig(tx *pg.Tx, model []*GlobalAuthorisationConfig) error { + err := tx.Insert(&model) + if err != nil { + repo.logger.Errorw("error in creating global authorisation config", "err", err) + return err + } + return nil +} + +func (repo *GlobalAuthorisationConfigRepositoryImpl) UpdateConfig(tx *pg.Tx, model []*GlobalAuthorisationConfig) error { + _, err := tx.Model(&model).Update() + if err != nil { + repo.logger.Errorw("error in updating global authorisation config", "err", err) + return err + } + return nil +} + +func (repo *GlobalAuthorisationConfigRepositoryImpl) SetConfigsToInactiveExceptGivenConfigs(tx *pg.Tx, configTypes []string) error { + var model []*GlobalAuthorisationConfig + _, err := tx.Model(&model).Set("active = ?", false).Where("config_type not in (?)", pg.In(configTypes)). + Update() + if err != nil { + repo.logger.Errorw("error in updating configs to inactive except configs", "err", err, "configTypes", configTypes) + return err + } + return err +} diff --git a/pkg/auth/sso/SSOLoginService.go b/pkg/auth/sso/SSOLoginService.go index 9df62522b1..0c18cc418c 100644 --- a/pkg/auth/sso/SSOLoginService.go +++ b/pkg/auth/sso/SSOLoginService.go @@ -19,6 +19,7 @@ package sso import ( "encoding/json" "fmt" + globalConfig "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig" "github.com/devtron-labs/devtron/pkg/auth/user/bean" "time" @@ -40,11 +41,12 @@ type SSOLoginService interface { } type SSOLoginServiceImpl struct { - logger *zap.SugaredLogger - ssoLoginRepository SSOLoginRepository - K8sUtil *k8s.K8sServiceImpl - devtronSecretConfig *util2.DevtronSecretConfig - userAuthOidcHelper authentication.UserAuthOidcHelper + logger *zap.SugaredLogger + ssoLoginRepository SSOLoginRepository + K8sUtil *k8s.K8sServiceImpl + devtronSecretConfig *util2.DevtronSecretConfig + userAuthOidcHelper authentication.UserAuthOidcHelper + globalAuthConfigService globalConfig.GlobalAuthorisationConfigService } type Config struct { @@ -99,13 +101,15 @@ const ( func NewSSOLoginServiceImpl( logger *zap.SugaredLogger, ssoLoginRepository SSOLoginRepository, - K8sUtil *k8s.K8sServiceImpl, envVariables *util2.EnvironmentVariables, userAuthOidcHelper authentication.UserAuthOidcHelper) *SSOLoginServiceImpl { + K8sUtil *k8s.K8sServiceImpl, envVariables *util2.EnvironmentVariables, userAuthOidcHelper authentication.UserAuthOidcHelper, + globalAuthConfigService globalConfig.GlobalAuthorisationConfigService) *SSOLoginServiceImpl { serviceImpl := &SSOLoginServiceImpl{ - logger: logger, - ssoLoginRepository: ssoLoginRepository, - K8sUtil: K8sUtil, - devtronSecretConfig: envVariables.DevtronSecretConfig, - userAuthOidcHelper: userAuthOidcHelper, + logger: logger, + ssoLoginRepository: ssoLoginRepository, + K8sUtil: K8sUtil, + devtronSecretConfig: envVariables.DevtronSecretConfig, + userAuthOidcHelper: userAuthOidcHelper, + globalAuthConfigService: globalAuthConfigService, } return serviceImpl } @@ -156,6 +160,15 @@ func (impl SSOLoginServiceImpl) CreateSSOLogin(request *bean.SSOLoginDto) (*bean return nil, err } request.Id = model.Id + + // updating/creating globalAuthConfig here, doing this here to improve user experience, but altogether they are different components + if len(request.GlobalAuthConfigType) > 0 { + _, err = impl.globalAuthConfigService.CreateOrUpdateGroupClaimsAuthConfig(tx, request.GlobalAuthConfigType, request.UserId) + if err != nil { + impl.logger.Errorw("error in CreateOrUpdateGroupClaimsAuthConfig", "err", err) + return nil, err + } + } _, err = impl.updateDexConfig(request) if err != nil { impl.logger.Errorw("error in creating new sso login config", "error", err) @@ -169,7 +182,8 @@ func (impl SSOLoginServiceImpl) CreateSSOLogin(request *bean.SSOLoginDto) (*bean // update in memory data on sso add-update impl.userAuthOidcHelper.UpdateInMemoryDataOnSsoAddUpdate(request.Url) - + // Updating cache for globalAuthConfig + impl.globalAuthConfigService.ReloadCache() return request, nil } @@ -241,6 +255,15 @@ func (impl SSOLoginServiceImpl) UpdateSSOLogin(request *bean.SSOLoginDto) (*bean return nil, err } request.Config = newConfigString + + // updating/creating globalAuthConfig here, doing this here to improve user experience, but altogether they are different components + if len(request.GlobalAuthConfigType) > 0 { + _, err = impl.globalAuthConfigService.CreateOrUpdateGroupClaimsAuthConfig(tx, request.GlobalAuthConfigType, request.UserId) + if err != nil { + impl.logger.Errorw("error in CreateOrUpdateGroupClaimsAuthConfig", "err", err, "globalAuthConfigType", request.GlobalAuthConfigType) + return nil, err + } + } _, err = impl.updateDexConfig(request) if err != nil { impl.logger.Errorw("error in creating new sso login config", "error", err) @@ -254,7 +277,8 @@ func (impl SSOLoginServiceImpl) UpdateSSOLogin(request *bean.SSOLoginDto) (*bean // update in memory data on sso add-update impl.userAuthOidcHelper.UpdateInMemoryDataOnSsoAddUpdate(request.Url) - + // Updating cache for globalAuthConfig + impl.globalAuthConfigService.ReloadCache() return request, nil } diff --git a/pkg/auth/user/UserSelfRegistrationService.go b/pkg/auth/user/UserSelfRegistrationService.go index 7a13b105e2..5add86f274 100644 --- a/pkg/auth/user/UserSelfRegistrationService.go +++ b/pkg/auth/user/UserSelfRegistrationService.go @@ -18,7 +18,7 @@ package user import ( "fmt" - jwt2 "github.com/devtron-labs/authenticator/jwt" + globalConfig "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig" "github.com/devtron-labs/devtron/pkg/auth/user/adapter" "github.com/devtron-labs/devtron/pkg/auth/user/bean" "github.com/devtron-labs/devtron/pkg/auth/user/repository" @@ -28,22 +28,25 @@ import ( type UserSelfRegistrationService interface { CheckSelfRegistrationRoles() (CheckResponse, error) - SelfRegister(emailId string) (*bean.UserInfo, error) + SelfRegister(emailId string, groups []string) (*bean.UserInfo, error) CheckAndCreateUserIfConfigured(claims jwt.MapClaims) bool } type UserSelfRegistrationServiceImpl struct { - logger *zap.SugaredLogger - selfRegistrationRolesRepository repository.SelfRegistrationRolesRepository - userService UserService + logger *zap.SugaredLogger + selfRegistrationRolesRepository repository.SelfRegistrationRolesRepository + userService UserService + globalAuthorisationConfigService globalConfig.GlobalAuthorisationConfigService } func NewUserSelfRegistrationServiceImpl(logger *zap.SugaredLogger, - selfRegistrationRolesRepository repository.SelfRegistrationRolesRepository, userService UserService) *UserSelfRegistrationServiceImpl { + selfRegistrationRolesRepository repository.SelfRegistrationRolesRepository, userService UserService, + globalAuthorisationConfigService globalConfig.GlobalAuthorisationConfigService) *UserSelfRegistrationServiceImpl { return &UserSelfRegistrationServiceImpl{ - logger: logger, - selfRegistrationRolesRepository: selfRegistrationRolesRepository, - userService: userService, + logger: logger, + selfRegistrationRolesRepository: selfRegistrationRolesRepository, + userService: userService, + globalAuthorisationConfigService: globalAuthorisationConfigService, } } @@ -75,13 +78,11 @@ func (impl *UserSelfRegistrationServiceImpl) CheckSelfRegistrationRoles() (Check checkResponse.Enabled = false return checkResponse, err } - //var roles []string if roleEntries != nil { for _, role := range roleEntries { if role.Role != "" { checkResponse.Roles = append(checkResponse.Roles, role.Role) checkResponse.Enabled = true - //return checkResponse, err } } if checkResponse.Enabled == true { @@ -94,47 +95,70 @@ func (impl *UserSelfRegistrationServiceImpl) CheckSelfRegistrationRoles() (Check return checkResponse, nil } -func (impl *UserSelfRegistrationServiceImpl) SelfRegister(emailId string) (*bean.UserInfo, error) { - roles, err := impl.CheckSelfRegistrationRoles() - if err != nil || roles.Enabled == false { - return nil, err - } - impl.logger.Infow("self register start") - userInfo := &bean.UserInfo{ - EmailId: emailId, - Roles: roles.Roles, - SuperAdmin: false, - } - - userInfos, err := impl.userService.SelfRegisterUserIfNotExists(adapter.BuildSelfRegisterDto(userInfo)) - if err != nil { - impl.logger.Errorw("error while register user", "error", err) - return nil, err +func (impl *UserSelfRegistrationServiceImpl) SelfRegister(emailId string, groups []string) (*bean.UserInfo, error) { + toSelfRegisterUser := false + var selfRegistrationRoles []string + groupClaimsConfigActive := impl.globalAuthorisationConfigService.IsGroupClaimsConfigActive() + if groupClaimsConfigActive { + toSelfRegisterUser = true + selfRegistrationRoles = nil //just for easy readability + } else { + roles, err := impl.CheckSelfRegistrationRoles() + if err != nil { + return nil, err + } + if roles.Enabled { + toSelfRegisterUser = true + selfRegistrationRoles = roles.Roles + } } - impl.logger.Errorw("registerd user", "user", userInfos) - if len(userInfos) > 0 { - return userInfos[0], nil + if toSelfRegisterUser { + impl.logger.Infow("self register start") + userInfo := &bean.UserInfo{ + EmailId: emailId, + Roles: selfRegistrationRoles, + SuperAdmin: false, + } + userInfos, err := impl.userService.SelfRegisterUserIfNotExists(adapter.BuildSelfRegisterDto(userInfo, groups, groupClaimsConfigActive)) + if err != nil { + impl.logger.Errorw("error while register user", "error", err) + return nil, err + } + impl.logger.Infow("self registered user", "user", userInfos) + if len(userInfos) > 0 { + return userInfos[0], nil + } else { + return nil, fmt.Errorf("user not created") + } } else { - return nil, fmt.Errorf("user not created") + return nil, nil } } func (impl *UserSelfRegistrationServiceImpl) CheckAndCreateUserIfConfigured(claims jwt.MapClaims) bool { - emailId := jwt2.GetField(claims, "email") - sub := jwt2.GetField(claims, "sub") - if emailId == "" && sub == "admin" { - emailId = sub - } + emailId, groups := impl.globalAuthorisationConfigService.GetEmailAndGroupsFromClaims(claims) + impl.logger.Info("check and create user if configured") exists := impl.userService.UserExists(emailId) if !exists { impl.logger.Infow("self registering user, ", "email", emailId) - user, err := impl.SelfRegister(emailId) + user, err := impl.SelfRegister(emailId, groups) if err != nil { impl.logger.Errorw("error while register user", "error", err) } else if user != nil && user.Id > 0 { exists = true } } + if exists { + groupClaimsConfigActive := impl.globalAuthorisationConfigService.IsGroupClaimsConfigActive() + //user is active, need to update group claim data if needed + if groupClaimsConfigActive { + err := impl.userService.UpdateUserGroupMappingIfActiveUser(emailId, groups) + if err != nil { + impl.logger.Errorw("error in updating data for user group claims map", "err", err, "emailId", emailId) + return exists + } + } + } impl.logger.Infow("user status", "email", emailId, "status", exists) return exists } diff --git a/pkg/auth/user/UserService.go b/pkg/auth/user/UserService.go index 5be9e8f017..8bca888faf 100644 --- a/pkg/auth/user/UserService.go +++ b/pkg/auth/user/UserService.go @@ -26,6 +26,7 @@ import ( "time" bean4 "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin/bean" + globalConfig "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig" "github.com/devtron-labs/devtron/pkg/auth/user/adapter" userHelper "github.com/devtron-labs/devtron/pkg/auth/user/helper" adapter2 "github.com/devtron-labs/devtron/pkg/auth/user/repository/adapter" @@ -78,6 +79,8 @@ type UserService interface { DeleteUser(userInfo *userBean.UserInfo) (bool, error) BulkDeleteUsers(request *userBean.BulkDeleteRequest) (bool, error) CheckUserRoles(id int32, token string) ([]string, error) + UpdateUserGroupMappingIfActiveUser(emailId string, groups []string) error + GetEmailAndGroupClaimsFromToken(token string) (string, []string, error) SyncOrchestratorToCasbin() (bool, error) GetUserByToken(context context.Context, token string) (int32, string, error) //IsSuperAdmin(userId int) (bool, error) @@ -94,15 +97,17 @@ type UserServiceImpl struct { userReqLock sync.RWMutex //map of userId and current lock-state of their serving ability; //if TRUE then it means that some request is ongoing & unable to serve and FALSE then it is open to serve - userReqState map[int32]bool - userAuthRepository repository.UserAuthRepository - logger *zap.SugaredLogger - userRepository repository.UserRepository - roleGroupRepository repository.RoleGroupRepository - sessionManager2 *middleware.SessionManager - userCommonService UserCommonService - userAuditService UserAuditService - roleGroupService RoleGroupService + userReqState map[int32]bool + userAuthRepository repository.UserAuthRepository + logger *zap.SugaredLogger + userRepository repository.UserRepository + roleGroupRepository repository.RoleGroupRepository + sessionManager2 *middleware.SessionManager + userCommonService UserCommonService + userAuditService UserAuditService + roleGroupService RoleGroupService + userGroupMapRepository repository.UserAutoAssignGroupMapRepository + globalAuthorisationConfigService globalConfig.GlobalAuthorisationConfigService } func NewUserServiceImpl(userAuthRepository repository.UserAuthRepository, @@ -110,17 +115,21 @@ func NewUserServiceImpl(userAuthRepository repository.UserAuthRepository, userRepository repository.UserRepository, userGroupRepository repository.RoleGroupRepository, sessionManager2 *middleware.SessionManager, userCommonService UserCommonService, userAuditService UserAuditService, - roleGroupService RoleGroupService) *UserServiceImpl { + roleGroupService RoleGroupService, + userGroupMapRepository repository.UserAutoAssignGroupMapRepository, + globalAuthorisationConfigService globalConfig.GlobalAuthorisationConfigService) *UserServiceImpl { serviceImpl := &UserServiceImpl{ - userReqState: make(map[int32]bool), - userAuthRepository: userAuthRepository, - logger: logger, - userRepository: userRepository, - roleGroupRepository: userGroupRepository, - sessionManager2: sessionManager2, - userCommonService: userCommonService, - userAuditService: userAuditService, - roleGroupService: roleGroupService, + userReqState: make(map[int32]bool), + userAuthRepository: userAuthRepository, + logger: logger, + userRepository: userRepository, + roleGroupRepository: userGroupRepository, + sessionManager2: sessionManager2, + userCommonService: userCommonService, + userAuditService: userAuditService, + roleGroupService: roleGroupService, + userGroupMapRepository: userGroupMapRepository, + globalAuthorisationConfigService: globalAuthorisationConfigService, } cStore = sessions.NewCookieStore(randKey()) return serviceImpl diff --git a/pkg/auth/user/UserService_ent.go b/pkg/auth/user/UserService_ent.go index efb0a333cb..9029b2cd2d 100644 --- a/pkg/auth/user/UserService_ent.go +++ b/pkg/auth/user/UserService_ent.go @@ -1,18 +1,47 @@ +/* + * 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 user import ( "fmt" + "github.com/devtron-labs/authenticator/jwt" casbin2 "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin" bean4 "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin/bean" "github.com/devtron-labs/devtron/pkg/auth/user/adapter" userBean "github.com/devtron-labs/devtron/pkg/auth/user/bean" userrepo "github.com/devtron-labs/devtron/pkg/auth/user/repository" + util3 "github.com/devtron-labs/devtron/pkg/auth/user/util" + "github.com/devtron-labs/devtron/pkg/sql" "github.com/go-pg/pg" + jwtv4 "github.com/golang-jwt/jwt/v4" "github.com/juju/errors" "strings" + "time" ) func (impl *UserServiceImpl) UpdateDataForGroupClaims(dto *userBean.SelfRegisterDto) error { + userInfo := dto.UserInfo + if dto.GroupClaimsConfigActive { + err := impl.updateDataForUserGroupClaimsMap(userInfo.Id, dto.GroupsFromClaims) + if err != nil { + impl.logger.Errorw("error in updating data for user group claims map", "err", err, "userId", userInfo.Id) + return err + } + } return nil } @@ -93,11 +122,14 @@ func (impl *UserServiceImpl) CheckUserRoles(id int32, token string) ([]string, e return nil, err } - groups, err := casbin2.GetRolesForUser(model.EmailId) + var groups []string + // devtron-system-managed path: get roles from casbin directly + activeRoles, err := casbin2.GetRolesForUser(model.EmailId) if err != nil { impl.logger.Errorw("No Roles Found for user", "id", model.Id) return nil, err } + groups = append(groups, activeRoles...) if len(groups) > 0 { // getting unique, handling for duplicate roles roleFromGroups, err := impl.getUniquesRolesByGroupCasbinNames(groups) @@ -108,9 +140,229 @@ func (impl *UserServiceImpl) CheckUserRoles(id int32, token string) ([]string, e groups = append(groups, roleFromGroups...) } + // group claims path: check group claims active and add role groups from JWT claims + isGroupClaimsActive := impl.globalAuthorisationConfigService.IsGroupClaimsConfigActive() + if isGroupClaimsActive && !strings.HasPrefix(model.EmailId, userBean.API_TOKEN_USER_EMAIL_PREFIX) { + _, groupClaims, err := impl.GetEmailAndGroupClaimsFromToken(token) + if err != nil { + impl.logger.Errorw("error in GetEmailAndGroupClaimsFromToken", "err", err) + return nil, err + } + if len(groupClaims) > 0 { + groupsCasbinNames := util3.GetGroupCasbinName(groupClaims) + grps, err := impl.getUniquesRolesByGroupCasbinNames(groupsCasbinNames) + if err != nil { + impl.logger.Errorw("error in getUniquesRolesByGroupCasbinNames", "err", err) + return nil, err + } + groups = append(groups, grps...) + } + } + return groups, nil } +func (impl *UserServiceImpl) UpdateUserGroupMappingIfActiveUser(emailId string, groups []string) error { + user, err := impl.userRepository.FetchActiveUserByEmail(emailId) + if err != nil { + impl.logger.Errorw("error in getting active user by email", "err", err, "emailId", emailId) + return err + } + err = impl.updateDataForUserGroupClaimsMap(user.Id, groups) + if err != nil { + impl.logger.Errorw("error in updating data for user group claims map", "err", err, "userId", user.Id) + return err + } + return nil +} + +func (impl *UserServiceImpl) updateDataForUserGroupClaimsMap(userId int32, groups []string) error { + //updating groups received in claims + mapOfGroups := make(map[string]bool, len(groups)) + for _, group := range groups { + mapOfGroups[group] = true + } + groupMappings, err := impl.userGroupMapRepository.GetByUserId(userId) + if err != nil && err != pg.ErrNoRows { + impl.logger.Errorw("error in getting user group mapping by userId", "err", err, "userId", userId) + return err + } + dbConnection := impl.userGroupMapRepository.GetConnection() + tx, err := dbConnection.Begin() + if err != nil { + impl.logger.Errorw("error in initiating transaction", "err", err) + return err + } + defer tx.Rollback() + modelsToBeSaved := make([]*userrepo.UserAutoAssignedGroup, 0) + modelsToBeUpdated := make([]*userrepo.UserAutoAssignedGroup, 0) + timeNow := time.Now() + for i := range groupMappings { + groupMapping := groupMappings[i] + //checking if mapping present in groups from claims + if _, ok := mapOfGroups[groupMapping.GroupName]; ok { + //present so marking active flag true + groupMapping.Active = true + //deleting entry from map now + delete(mapOfGroups, groupMapping.GroupName) + } else { + //not present so marking active flag false + groupMapping.Active = false + } + groupMapping.UpdatedOn = timeNow + groupMapping.UpdatedBy = userBean.SystemUserId //system user + + //adding this group mapping to updated models irrespective of active + modelsToBeUpdated = append(modelsToBeUpdated, groupMapping) + } + + //iterating through remaining groups from the map, they are not found in current entries so need to be saved + for group := range mapOfGroups { + modelsToBeSaved = append(modelsToBeSaved, &userrepo.UserAutoAssignedGroup{ + UserId: userId, + GroupName: group, + IsGroupClaimsData: true, + Active: true, + AuditLog: sql.AuditLog{ + CreatedBy: 1, + CreatedOn: timeNow, + UpdatedBy: 1, + UpdatedOn: timeNow, + }, + }) + } + if len(modelsToBeUpdated) > 0 { + err = impl.userGroupMapRepository.Update(modelsToBeUpdated, tx) + if err != nil { + impl.logger.Errorw("error in updating user group mapping", "err", err) + return err + } + } + if len(modelsToBeSaved) > 0 { + err = impl.userGroupMapRepository.Save(modelsToBeSaved, tx) + if err != nil { + impl.logger.Errorw("error in saving user group mapping", "err", err) + return err + } + } + err = tx.Commit() + if err != nil { + impl.logger.Errorw("error in committing transaction", "err", err) + return err + } + return nil +} + +func (impl *UserServiceImpl) getRoleFiltersForGroupClaims(id int32) ([]userBean.RoleFilter, error) { + var roleFilters []userBean.RoleFilter + userGroups, err := impl.userGroupMapRepository.GetActiveByUserId(id) + if err != nil { + impl.logger.Errorw("error in GetActiveByUserId", "err", err, "userId", id) + return nil, err + } + groupClaims := make([]string, 0, len(userGroups)) + for _, userGroup := range userGroups { + groupClaims = append(groupClaims, userGroup.GroupName) + } + // checking by group casbin name (considering case insensitivity here) + if len(groupClaims) > 0 { + groupCasbinNames := util3.GetGroupCasbinName(groupClaims) + groupFilters, err := impl.GetRoleFiltersByGroupCasbinNames(groupCasbinNames) + if err != nil { + impl.logger.Errorw("error while GetRoleFiltersByGroupCasbinNames", "error", err, "groupCasbinNames", groupCasbinNames) + return nil, err + } + if len(groupFilters) > 0 { + roleFilters = append(roleFilters, groupFilters...) + } + } + return roleFilters, nil +} + +func (impl *UserServiceImpl) getRoleGroupsForGroupClaims(id int32) ([]userBean.UserRoleGroup, error) { + userGroups, err := impl.userGroupMapRepository.GetActiveByUserId(id) + if err != nil { + impl.logger.Errorw("error in GetActiveByUserId", "err", err, "userId", id) + return nil, err + } + groupClaims := make([]string, 0, len(userGroups)) + for _, userGroup := range userGroups { + groupClaims = append(groupClaims, userGroup.GroupName) + } + // checking by group casbin name (considering case insensitivity here) + var userRoleGroups []userBean.UserRoleGroup + if len(groupClaims) > 0 { + groupCasbinNames := util3.GetGroupCasbinName(groupClaims) + userRoleGroups, err = impl.fetchUserRoleGroupsByGroupClaims(groupCasbinNames) + if err != nil { + impl.logger.Errorw("error in fetchUserRoleGroupsByGroupClaims ", "err", err, "groupClaims", groupClaims) + return nil, err + } + } + return userRoleGroups, nil +} + +func (impl *UserServiceImpl) fetchUserRoleGroupsByGroupClaims(groupCasbinNames []string) ([]userBean.UserRoleGroup, error) { + roleGroups, err := impl.roleGroupRepository.GetRoleGroupListByCasbinNames(groupCasbinNames) + if err != nil { + impl.logger.Errorw("error in fetchUserRoleGroupsByGroupClaims", "err", err, "groupCasbinNames", groupCasbinNames) + return nil, err + } + userRoleGroups := make([]userBean.UserRoleGroup, 0, len(roleGroups)) + for _, roleGroup := range roleGroups { + userRoleGroups = append(userRoleGroups, userBean.UserRoleGroup{ + RoleGroup: &userBean.RoleGroup{ + Id: roleGroup.Id, + Name: roleGroup.Name, + Description: roleGroup.Description, + }, + }) + } + return userRoleGroups, nil +} + +// GetRoleFiltersByGroupCasbinNames returns role filters for the given group casbin names +func (impl *UserServiceImpl) GetRoleFiltersByGroupCasbinNames(groupCasbinNames []string) ([]userBean.RoleFilter, error) { + roles, err := impl.roleGroupRepository.GetRolesByGroupCasbinNames(groupCasbinNames) + if err != nil && err != pg.ErrNoRows { + impl.logger.Errorw("error in getting roles by group casbin names", "err", err) + return nil, err + } + var roleFilters []userBean.RoleFilter + // merging considering env as base first + roleFilters = impl.userCommonService.BuildRoleFiltersAfterMerging(ConvertRolesToEntityProcessors(roles), userBean.EnvironmentBasedKey) + // merging role filters based on application + roleFilters = impl.userCommonService.BuildRoleFiltersAfterMerging(ConvertRoleFiltersToEntityProcessors(roleFilters), userBean.ApplicationBasedKey) + return roleFilters, nil +} + +// GetEmailAndGroupClaimsFromToken extracts email and group claims from the JWT token +func (impl *UserServiceImpl) GetEmailAndGroupClaimsFromToken(token string) (string, []string, error) { + if token == "" { + return "", nil, nil + } + mapClaims, err := impl.getMapClaims(token) + if err != nil { + return "", nil, err + } + email, groups := impl.globalAuthorisationConfigService.GetEmailAndGroupsFromClaims(mapClaims) + return email, groups, nil +} + +func (impl *UserServiceImpl) getMapClaims(token string) (jwtv4.MapClaims, error) { + claims, err := impl.sessionManager2.VerifyToken(token) + if err != nil { + impl.logger.Errorw("failed to verify token", "error", err) + return nil, err + } + mapClaims, err := jwt.MapClaims(claims) + if err != nil { + impl.logger.Errorw("failed to MapClaims", "error", err) + return nil, err + } + return mapClaims, nil +} + func (impl *UserServiceImpl) getUserGroupMapFromModels(model []userrepo.UserModel) (*userBean.UserGroupMapDto, error) { return nil, nil } @@ -121,7 +373,6 @@ func setTwcId(model *userrepo.UserModel, twcId int) { func (impl *UserServiceImpl) getTimeoutWindowID(tx *pg.Tx, userInfo *userBean.UserInfo) (int, error) { return 0, nil - } // createOrUpdateUserRoleGroupsPolices : gives policies which are to be added and which are to be eliminated from casbin, with support of timewindow Config changed fromm existing diff --git a/pkg/auth/user/adapter/adapter.go b/pkg/auth/user/adapter/adapter.go index f20cee0685..00d6642ef9 100644 --- a/pkg/auth/user/adapter/adapter.go +++ b/pkg/auth/user/adapter/adapter.go @@ -51,9 +51,11 @@ func BuildUserInfoResponseAdapter(requestUserInfo *bean2.UserInfo, emailId strin } } -func BuildSelfRegisterDto(userInfo *bean2.UserInfo) *bean2.SelfRegisterDto { +func BuildSelfRegisterDto(userInfo *bean2.UserInfo, groups []string, groupClaimsConfigActive bool) *bean2.SelfRegisterDto { return &bean2.SelfRegisterDto{ - UserInfo: userInfo, + UserInfo: userInfo, + GroupsFromClaims: groups, + GroupClaimsConfigActive: groupClaimsConfigActive, } } diff --git a/pkg/auth/user/bean/UserRequest.go b/pkg/auth/user/bean/UserRequest.go index a60c1ee2cd..ece6dabfff 100644 --- a/pkg/auth/user/bean/UserRequest.go +++ b/pkg/auth/user/bean/UserRequest.go @@ -111,13 +111,14 @@ type RoleData struct { } type SSOLoginDto struct { - Id int32 `json:"id"` - Name string `json:"name,omitempty," validate:"validate-sso-config-name"` - Label string `json:"label,omitempty"` - Url string `json:"url,omitempty"` - Config json.RawMessage `json:"config,omitempty"` - Active bool `json:"active"` - UserId int32 `json:"-"` + Id int32 `json:"id"` + Name string `json:"name,omitempty," validate:"validate-sso-config-name"` + Label string `json:"label,omitempty"` + Url string `json:"url,omitempty"` + Config json.RawMessage `json:"config,omitempty"` + Active bool `json:"active"` + UserId int32 `json:"-"` + GlobalAuthConfigType string `json:"globalAuthConfigType"` } const ( @@ -207,7 +208,9 @@ func (pa *UserPermissionsAuditDto) WithEntityAudit(entityAudit sql.AuditLog) *Us } type SelfRegisterDto struct { - UserInfo *UserInfo + UserInfo *UserInfo + GroupsFromClaims []string + GroupClaimsConfigActive bool } type TimeoutWindowConfigDto struct { diff --git a/pkg/auth/user/repository/UserAutoAssignGroupMapRepository.go b/pkg/auth/user/repository/UserAutoAssignGroupMapRepository.go new file mode 100644 index 0000000000..202464a089 --- /dev/null +++ b/pkg/auth/user/repository/UserAutoAssignGroupMapRepository.go @@ -0,0 +1,112 @@ +/* + * 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 repository + +import ( + "github.com/devtron-labs/devtron/pkg/sql" + "github.com/go-pg/pg" + "github.com/go-pg/pg/orm" + "go.uber.org/zap" +) + +type UserAutoAssignGroupMapRepository interface { + GetConnection() *pg.DB + GetByUserId(userId int32) ([]*UserAutoAssignedGroup, error) + Save(models []*UserAutoAssignedGroup, tx *pg.Tx) error + Update(models []*UserAutoAssignedGroup, tx *pg.Tx) error + GetActiveByUserId(userId int32) ([]*UserAutoAssignedGroup, error) + GetAllActiveWithUser() ([]*UserAutoAssignedGroup, error) +} + +type UserAutoAssignGroupMapRepositoryImpl struct { + dbConnection *pg.DB + logger *zap.SugaredLogger +} + +func NewUserAutoAssignGroupMapRepositoryImpl(dbConnection *pg.DB, logger *zap.SugaredLogger) *UserAutoAssignGroupMapRepositoryImpl { + return &UserAutoAssignGroupMapRepositoryImpl{dbConnection: dbConnection, logger: logger} +} + +type UserAutoAssignedGroup struct { + TableName struct{} `sql:"user_auto_assigned_groups"` + Id int `sql:"id,pk"` + UserId int32 `sql:"user_id"` + GroupName string `sql:"group_name,notnull"` + IsGroupClaimsData bool `sql:"is_group_claims_data,notnull"` + Active bool `sql:"active,notnull"` + User *UserModel `pg:"fk:user_id"` + sql.AuditLog +} + +func (repo *UserAutoAssignGroupMapRepositoryImpl) GetConnection() *pg.DB { + return repo.dbConnection +} + +func (repo *UserAutoAssignGroupMapRepositoryImpl) GetByUserId(userId int32) ([]*UserAutoAssignedGroup, error) { + var models []*UserAutoAssignedGroup + err := repo.dbConnection.Model(&models).Where("user_id = ?", userId).Select() + if err != nil { + repo.logger.Errorw("error, GetByUserId", "err", err, "userId", userId) + return nil, err + } + return models, nil +} + +func (repo *UserAutoAssignGroupMapRepositoryImpl) Save(models []*UserAutoAssignedGroup, tx *pg.Tx) error { + err := tx.Insert(&models) + if err != nil { + repo.logger.Errorw("error, Save", "err", err, "models", models) + return err + } + return nil +} + +func (repo *UserAutoAssignGroupMapRepositoryImpl) Update(models []*UserAutoAssignedGroup, tx *pg.Tx) error { + _, err := tx.Model(&models).Update() + if err != nil { + repo.logger.Errorw("error, UpdateInBatch", "err", err, "models", models) + return err + } + return nil +} + +func (repo *UserAutoAssignGroupMapRepositoryImpl) GetActiveByUserId(userId int32) ([]*UserAutoAssignedGroup, error) { + var models []*UserAutoAssignedGroup + err := repo.dbConnection.Model(&models).Where("user_id = ?", userId). + Where("active = ?", true).Select() + if err != nil { + repo.logger.Errorw("error, GetActiveByUserId", "err", err, "userId", userId) + return nil, err + } + return models, nil +} + +func (repo *UserAutoAssignGroupMapRepositoryImpl) GetAllActiveWithUser() ([]*UserAutoAssignedGroup, error) { + var models []*UserAutoAssignedGroup + err := repo.dbConnection.Model(&models). + Column("user_auto_assigned_group.*"). + Relation("User", func(q *orm.Query) (*orm.Query, error) { + return q.Where("\"user\".active = ?", true), nil + }). + Where("user_auto_assigned_group.active = ?", true). + Select() + if err != nil { + repo.logger.Errorw("error, GetAllActiveWithUser", "err", err) + return nil, err + } + return models, nil +} diff --git a/pkg/auth/user/util/util.go b/pkg/auth/user/util/util.go index d958dc5418..f99aaa5283 100644 --- a/pkg/auth/user/util/util.go +++ b/pkg/auth/user/util/util.go @@ -18,6 +18,7 @@ package util import ( "context" + "fmt" "github.com/devtron-labs/devtron/pkg/auth/user/bean" util2 "github.com/devtron-labs/devtron/util" "strings" @@ -27,6 +28,15 @@ const ( ApiTokenPrefix = "API-TOKEN:" ) +func GetGroupCasbinName(groups []string) []string { + groupCasbinNames := make([]string, 0) + for _, group := range groups { + toLowerGroup := strings.ToLower(group) + groupCasbinNames = append(groupCasbinNames, fmt.Sprintf("group:%s", strings.ReplaceAll(toLowerGroup, " ", "_"))) + } + return groupCasbinNames +} + func CheckValidationForRoleGroupCreation(name string) bool { if strings.Contains(name, ",") { return false diff --git a/wire_gen.go b/wire_gen.go index 9b6a1b2e7e..868b4d10fd 100644 --- a/wire_gen.go +++ b/wire_gen.go @@ -81,8 +81,8 @@ import ( config3 "github.com/devtron-labs/devtron/client/argocdServer/config" "github.com/devtron-labs/devtron/client/argocdServer/connection" "github.com/devtron-labs/devtron/client/argocdServer/repoCredsK8sClient" - repository7 "github.com/devtron-labs/devtron/client/argocdServer/repocreds" - repository6 "github.com/devtron-labs/devtron/client/argocdServer/repository" + repository8 "github.com/devtron-labs/devtron/client/argocdServer/repocreds" + repository7 "github.com/devtron-labs/devtron/client/argocdServer/repository" "github.com/devtron-labs/devtron/client/argocdServer/version" cron2 "github.com/devtron-labs/devtron/client/cron" "github.com/devtron-labs/devtron/client/dashboard" @@ -99,9 +99,9 @@ import ( "github.com/devtron-labs/devtron/internal/sql/repository/appWorkflow" "github.com/devtron-labs/devtron/internal/sql/repository/chartConfig" "github.com/devtron-labs/devtron/internal/sql/repository/deploymentConfig" - repository9 "github.com/devtron-labs/devtron/internal/sql/repository/dockerRegistry" + repository10 "github.com/devtron-labs/devtron/internal/sql/repository/dockerRegistry" "github.com/devtron-labs/devtron/internal/sql/repository/helper" - repository24 "github.com/devtron-labs/devtron/internal/sql/repository/imageTagging" + repository25 "github.com/devtron-labs/devtron/internal/sql/repository/imageTagging" "github.com/devtron-labs/devtron/internal/sql/repository/pipelineConfig" "github.com/devtron-labs/devtron/internal/sql/repository/resourceGroup" "github.com/devtron-labs/devtron/internal/util" @@ -114,7 +114,7 @@ import ( "github.com/devtron-labs/devtron/pkg/appClone/batch" appStatus2 "github.com/devtron-labs/devtron/pkg/appStatus" "github.com/devtron-labs/devtron/pkg/appStore/chartGroup" - repository29 "github.com/devtron-labs/devtron/pkg/appStore/chartGroup/repository" + repository30 "github.com/devtron-labs/devtron/pkg/appStore/chartGroup/repository" "github.com/devtron-labs/devtron/pkg/appStore/chartProvider" "github.com/devtron-labs/devtron/pkg/appStore/discover/repository" service7 "github.com/devtron-labs/devtron/pkg/appStore/discover/service" @@ -138,6 +138,8 @@ import ( "github.com/devtron-labs/devtron/pkg/attributes" "github.com/devtron-labs/devtron/pkg/auth/authentication" "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin" + "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig" + repository5 "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig/repository" "github.com/devtron-labs/devtron/pkg/auth/sso" "github.com/devtron-labs/devtron/pkg/auth/user" repository4 "github.com/devtron-labs/devtron/pkg/auth/user/repository" @@ -146,18 +148,18 @@ import ( read17 "github.com/devtron-labs/devtron/pkg/build/artifacts/imageTagging/read" "github.com/devtron-labs/devtron/pkg/build/git/gitHost" read21 "github.com/devtron-labs/devtron/pkg/build/git/gitHost/read" - repository27 "github.com/devtron-labs/devtron/pkg/build/git/gitHost/repository" + repository28 "github.com/devtron-labs/devtron/pkg/build/git/gitHost/repository" read15 "github.com/devtron-labs/devtron/pkg/build/git/gitMaterial/read" - repository22 "github.com/devtron-labs/devtron/pkg/build/git/gitMaterial/repository" + repository23 "github.com/devtron-labs/devtron/pkg/build/git/gitMaterial/repository" "github.com/devtron-labs/devtron/pkg/build/git/gitProvider" read9 "github.com/devtron-labs/devtron/pkg/build/git/gitProvider/read" - repository12 "github.com/devtron-labs/devtron/pkg/build/git/gitProvider/repository" + repository13 "github.com/devtron-labs/devtron/pkg/build/git/gitProvider/repository" "github.com/devtron-labs/devtron/pkg/build/git/gitWebhook" - repository11 "github.com/devtron-labs/devtron/pkg/build/git/gitWebhook/repository" + repository12 "github.com/devtron-labs/devtron/pkg/build/git/gitWebhook/repository" pipeline2 "github.com/devtron-labs/devtron/pkg/build/pipeline" read14 "github.com/devtron-labs/devtron/pkg/build/pipeline/read" "github.com/devtron-labs/devtron/pkg/build/trigger" - repository30 "github.com/devtron-labs/devtron/pkg/bulkAction/repository" + repository31 "github.com/devtron-labs/devtron/pkg/bulkAction/repository" service8 "github.com/devtron-labs/devtron/pkg/bulkAction/service" "github.com/devtron-labs/devtron/pkg/chart" "github.com/devtron-labs/devtron/pkg/chart/gitOpsConfig" @@ -170,7 +172,7 @@ import ( "github.com/devtron-labs/devtron/pkg/cluster/environment/repository" rbac2 "github.com/devtron-labs/devtron/pkg/cluster/rbac" read2 "github.com/devtron-labs/devtron/pkg/cluster/read" - repository5 "github.com/devtron-labs/devtron/pkg/cluster/repository" + repository6 "github.com/devtron-labs/devtron/pkg/cluster/repository" "github.com/devtron-labs/devtron/pkg/clusterTerminalAccess" "github.com/devtron-labs/devtron/pkg/commonService" "github.com/devtron-labs/devtron/pkg/config/configDiff" @@ -187,7 +189,7 @@ import ( "github.com/devtron-labs/devtron/pkg/deployment/manifest/configMapAndSecret" read20 "github.com/devtron-labs/devtron/pkg/deployment/manifest/configMapAndSecret/read" "github.com/devtron-labs/devtron/pkg/deployment/manifest/deployedAppMetrics" - repository17 "github.com/devtron-labs/devtron/pkg/deployment/manifest/deployedAppMetrics/repository" + repository18 "github.com/devtron-labs/devtron/pkg/deployment/manifest/deployedAppMetrics/repository" "github.com/devtron-labs/devtron/pkg/deployment/manifest/deploymentTemplate" "github.com/devtron-labs/devtron/pkg/deployment/manifest/deploymentTemplate/chartRef" read12 "github.com/devtron-labs/devtron/pkg/deployment/manifest/deploymentTemplate/chartRef/read" @@ -196,13 +198,13 @@ import ( "github.com/devtron-labs/devtron/pkg/deployment/manifest/publish" "github.com/devtron-labs/devtron/pkg/deployment/providerConfig" "github.com/devtron-labs/devtron/pkg/deployment/trigger/devtronApps" - repository26 "github.com/devtron-labs/devtron/pkg/deployment/trigger/devtronApps/userDeploymentRequest/repository" + repository27 "github.com/devtron-labs/devtron/pkg/deployment/trigger/devtronApps/userDeploymentRequest/repository" service4 "github.com/devtron-labs/devtron/pkg/deployment/trigger/devtronApps/userDeploymentRequest/service" "github.com/devtron-labs/devtron/pkg/deploymentGroup" "github.com/devtron-labs/devtron/pkg/devtronResource" "github.com/devtron-labs/devtron/pkg/devtronResource/history/deployment/cdPipeline" read10 "github.com/devtron-labs/devtron/pkg/devtronResource/read" - repository14 "github.com/devtron-labs/devtron/pkg/devtronResource/repository" + repository15 "github.com/devtron-labs/devtron/pkg/devtronResource/repository" "github.com/devtron-labs/devtron/pkg/dockerRegistry" "github.com/devtron-labs/devtron/pkg/eventProcessor" "github.com/devtron-labs/devtron/pkg/eventProcessor/celEvaluator" @@ -213,11 +215,11 @@ import ( "github.com/devtron-labs/devtron/pkg/fluxApplication" "github.com/devtron-labs/devtron/pkg/generateManifest" "github.com/devtron-labs/devtron/pkg/genericNotes" - repository10 "github.com/devtron-labs/devtron/pkg/genericNotes/repository" + repository11 "github.com/devtron-labs/devtron/pkg/genericNotes/repository" "github.com/devtron-labs/devtron/pkg/gitops" "github.com/devtron-labs/devtron/pkg/imageDigestPolicy" config4 "github.com/devtron-labs/devtron/pkg/infraConfig/config" - repository15 "github.com/devtron-labs/devtron/pkg/infraConfig/repository" + repository16 "github.com/devtron-labs/devtron/pkg/infraConfig/repository" "github.com/devtron-labs/devtron/pkg/infraConfig/repository/audit" service2 "github.com/devtron-labs/devtron/pkg/infraConfig/service" audit2 "github.com/devtron-labs/devtron/pkg/infraConfig/service/audit" @@ -226,7 +228,7 @@ import ( "github.com/devtron-labs/devtron/pkg/k8s/capacity" "github.com/devtron-labs/devtron/pkg/k8s/informer" "github.com/devtron-labs/devtron/pkg/kubernetesResourceAuditLogs" - repository28 "github.com/devtron-labs/devtron/pkg/kubernetesResourceAuditLogs/repository" + repository29 "github.com/devtron-labs/devtron/pkg/kubernetesResourceAuditLogs/repository" "github.com/devtron-labs/devtron/pkg/module" bean2 "github.com/devtron-labs/devtron/pkg/module/bean" "github.com/devtron-labs/devtron/pkg/module/read" @@ -240,21 +242,21 @@ import ( "github.com/devtron-labs/devtron/pkg/pipeline/draftAwareConfigService" "github.com/devtron-labs/devtron/pkg/pipeline/executors" "github.com/devtron-labs/devtron/pkg/pipeline/history" - repository23 "github.com/devtron-labs/devtron/pkg/pipeline/history/repository" + repository24 "github.com/devtron-labs/devtron/pkg/pipeline/history/repository" "github.com/devtron-labs/devtron/pkg/pipeline/infraProviders" "github.com/devtron-labs/devtron/pkg/pipeline/infraProviders/infraGetters/ci" "github.com/devtron-labs/devtron/pkg/pipeline/infraProviders/infraGetters/job" - repository20 "github.com/devtron-labs/devtron/pkg/pipeline/repository" + repository21 "github.com/devtron-labs/devtron/pkg/pipeline/repository" "github.com/devtron-labs/devtron/pkg/pipeline/types" "github.com/devtron-labs/devtron/pkg/pipeline/workflowStatus" - repository18 "github.com/devtron-labs/devtron/pkg/pipeline/workflowStatus/repository" + repository19 "github.com/devtron-labs/devtron/pkg/pipeline/workflowStatus/repository" "github.com/devtron-labs/devtron/pkg/plugin" - repository21 "github.com/devtron-labs/devtron/pkg/plugin/repository" + repository22 "github.com/devtron-labs/devtron/pkg/plugin/repository" "github.com/devtron-labs/devtron/pkg/policyGovernance/security/imageScanning" read19 "github.com/devtron-labs/devtron/pkg/policyGovernance/security/imageScanning/read" - repository25 "github.com/devtron-labs/devtron/pkg/policyGovernance/security/imageScanning/repository" + repository26 "github.com/devtron-labs/devtron/pkg/policyGovernance/security/imageScanning/repository" "github.com/devtron-labs/devtron/pkg/policyGovernance/security/scanTool" - repository16 "github.com/devtron-labs/devtron/pkg/policyGovernance/security/scanTool/repository" + repository17 "github.com/devtron-labs/devtron/pkg/policyGovernance/security/scanTool/repository" resourceGroup2 "github.com/devtron-labs/devtron/pkg/resourceGroup" "github.com/devtron-labs/devtron/pkg/resourceQualifiers" "github.com/devtron-labs/devtron/pkg/server" @@ -263,21 +265,21 @@ import ( "github.com/devtron-labs/devtron/pkg/sql" "github.com/devtron-labs/devtron/pkg/team" read4 "github.com/devtron-labs/devtron/pkg/team/read" - repository8 "github.com/devtron-labs/devtron/pkg/team/repository" + repository9 "github.com/devtron-labs/devtron/pkg/team/repository" "github.com/devtron-labs/devtron/pkg/terminal" "github.com/devtron-labs/devtron/pkg/ucid" "github.com/devtron-labs/devtron/pkg/userResource" util3 "github.com/devtron-labs/devtron/pkg/util" "github.com/devtron-labs/devtron/pkg/variables" "github.com/devtron-labs/devtron/pkg/variables/parsers" - repository13 "github.com/devtron-labs/devtron/pkg/variables/repository" + repository14 "github.com/devtron-labs/devtron/pkg/variables/repository" "github.com/devtron-labs/devtron/pkg/webhook/helm" "github.com/devtron-labs/devtron/pkg/workflow/cd" read18 "github.com/devtron-labs/devtron/pkg/workflow/cd/read" "github.com/devtron-labs/devtron/pkg/workflow/dag" status2 "github.com/devtron-labs/devtron/pkg/workflow/status" "github.com/devtron-labs/devtron/pkg/workflow/trigger/audit/hook" - repository19 "github.com/devtron-labs/devtron/pkg/workflow/trigger/audit/repository" + repository20 "github.com/devtron-labs/devtron/pkg/workflow/trigger/audit/repository" service3 "github.com/devtron-labs/devtron/pkg/workflow/trigger/audit/service" "github.com/devtron-labs/devtron/pkg/workflow/workflowStatusLatest" util2 "github.com/devtron-labs/devtron/util" @@ -356,11 +358,14 @@ func InitializeApp() (*App, error) { userAuditRepositoryImpl := repository4.NewUserAuditRepositoryImpl(db) userAuditServiceImpl := user.NewUserAuditServiceImpl(sugaredLogger, userAuditRepositoryImpl) roleGroupServiceImpl := user.NewRoleGroupServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, userCommonServiceImpl) - userServiceImpl := user.NewUserServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, sessionManager, userCommonServiceImpl, userAuditServiceImpl, roleGroupServiceImpl) + userAutoAssignGroupMapRepositoryImpl := repository4.NewUserAutoAssignGroupMapRepositoryImpl(db, sugaredLogger) + globalAuthorisationConfigRepositoryImpl := repository5.NewGlobalAuthorisationConfigRepositoryImpl(sugaredLogger, db) + globalAuthorisationConfigServiceImpl := globalConfig.NewGlobalAuthorisationConfigServiceImpl(sugaredLogger, globalAuthorisationConfigRepositoryImpl) + userServiceImpl := user.NewUserServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, sessionManager, userCommonServiceImpl, userAuditServiceImpl, roleGroupServiceImpl, userAutoAssignGroupMapRepositoryImpl, globalAuthorisationConfigServiceImpl) moduleRepositoryImpl := moduleRepo.NewModuleRepositoryImpl(db) moduleReadServiceImpl := read.NewModuleReadServiceImpl(sugaredLogger, moduleRepositoryImpl) gitOpsConfigReadServiceImpl := config.NewGitOpsConfigReadServiceImpl(sugaredLogger, gitOpsConfigRepositoryImpl, userServiceImpl, environmentVariables, moduleReadServiceImpl) - clusterRepositoryImpl := repository5.NewClusterRepositoryImpl(db, sugaredLogger, environmentVariables) + clusterRepositoryImpl := repository6.NewClusterRepositoryImpl(db, sugaredLogger, environmentVariables) k8sRuntimeConfig, err := k8s.GetRuntimeConfig() if err != nil { return nil, err @@ -399,9 +404,9 @@ func InitializeApp() (*App, error) { return nil, err } serviceClientImpl := application.NewApplicationClientImpl(sugaredLogger, argoCDConnectionManagerImpl) - repositoryServiceClientImpl := repository6.NewServiceClientImpl(sugaredLogger, argoCDConnectionManagerImpl) + repositoryServiceClientImpl := repository7.NewServiceClientImpl(sugaredLogger, argoCDConnectionManagerImpl) clusterServiceClientImpl := cluster2.NewServiceClientImpl(sugaredLogger, argoCDConnectionManagerImpl) - serviceClientImpl2 := repository7.NewServiceClientImpl(sugaredLogger, argoCDConnectionManagerImpl) + serviceClientImpl2 := repository8.NewServiceClientImpl(sugaredLogger, argoCDConnectionManagerImpl) certificateServiceClientImpl := certificate.NewServiceClientImpl(sugaredLogger, argoCDConnectionManagerImpl) acdConfig, err := argocdServer.GetACDDeploymentConfig() if err != nil { @@ -434,11 +439,11 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - enforcerImpl, err := casbin.NewEnforcerImpl(syncedEnforcer, casbinSyncedEnforcer, sessionManager, sugaredLogger) + enforcerImpl, err := casbin.NewEnforcerImpl(syncedEnforcer, casbinSyncedEnforcer, sessionManager, sugaredLogger, globalAuthorisationConfigServiceImpl) if err != nil { return nil, err } - teamRepositoryImpl := repository8.NewTeamRepositoryImpl(db) + teamRepositoryImpl := repository9.NewTeamRepositoryImpl(db) teamReadServiceImpl := read4.NewTeamReadService(sugaredLogger, teamRepositoryImpl) teamServiceImpl := team.NewTeamServiceImpl(sugaredLogger, teamRepositoryImpl, userAuthServiceImpl, teamReadServiceImpl) appRepositoryImpl := app.NewAppRepositoryImpl(db, sugaredLogger) @@ -480,9 +485,9 @@ func InitializeApp() (*App, error) { deploymentConfigServiceImpl := common.NewDeploymentConfigServiceImpl(repositoryImpl, sugaredLogger, chartRepositoryImpl, pipelineRepositoryImpl, appRepositoryImpl, installedAppReadServiceEAImpl, environmentVariables, envConfigOverrideReadServiceImpl, environmentRepositoryImpl, chartRefRepositoryImpl, deploymentConfigReadServiceImpl, acdAuthConfig) installedAppDBServiceImpl := EAMode.NewInstalledAppDBServiceImpl(sugaredLogger, installedAppRepositoryImpl, appRepositoryImpl, userServiceImpl, environmentServiceImpl, installedAppVersionHistoryRepositoryImpl, deploymentConfigServiceImpl) helmAppServiceImpl := service.NewHelmAppServiceImpl(sugaredLogger, clusterServiceImplExtended, helmAppClientImpl, pumpImpl, enforcerUtilHelmImpl, serverDataStoreServerDataStore, serverEnvConfigServerEnvConfig, appStoreApplicationVersionRepositoryImpl, environmentServiceImpl, pipelineRepositoryImpl, installedAppRepositoryImpl, appRepositoryImpl, clusterRepositoryImpl, k8sServiceImpl, helmReleaseConfig, helmAppReadServiceImpl, clusterReadServiceImpl, installedAppDBServiceImpl) - dockerArtifactStoreRepositoryImpl := repository9.NewDockerArtifactStoreRepositoryImpl(db, environmentVariables) - dockerRegistryIpsConfigRepositoryImpl := repository9.NewDockerRegistryIpsConfigRepositoryImpl(db) - ociRegistryConfigRepositoryImpl := repository9.NewOCIRegistryConfigRepositoryImpl(db) + dockerArtifactStoreRepositoryImpl := repository10.NewDockerArtifactStoreRepositoryImpl(db, environmentVariables) + dockerRegistryIpsConfigRepositoryImpl := repository10.NewDockerRegistryIpsConfigRepositoryImpl(db) + ociRegistryConfigRepositoryImpl := repository10.NewOCIRegistryConfigRepositoryImpl(db) dockerRegistryConfigImpl := pipeline.NewDockerRegistryConfigImpl(sugaredLogger, helmAppServiceImpl, dockerArtifactStoreRepositoryImpl, dockerRegistryIpsConfigRepositoryImpl, ociRegistryConfigRepositoryImpl, argoClientWrapperServiceImpl) deleteServiceExtendedImpl := delete2.NewDeleteServiceExtendedImpl(sugaredLogger, teamServiceImpl, clusterServiceImplExtended, environmentServiceImpl, appRepositoryImpl, environmentRepositoryImpl, pipelineRepositoryImpl, chartRepositoryServiceImpl, installedAppRepositoryImpl, dockerRegistryConfigImpl, dockerArtifactStoreRepositoryImpl, k8sServiceImpl, k8sInformerFactoryImpl) ciPipelineRepositoryImpl := pipelineConfig.NewCiPipelineRepositoryImpl(db, sugaredLogger, transactionUtilImpl) @@ -490,21 +495,21 @@ func InitializeApp() (*App, error) { commonEnforcementUtilImpl := commonEnforcementFunctionsUtil.NewCommonEnforcementUtilImpl(enforcerImpl, enforcerUtilImpl, sugaredLogger, userServiceImpl, userCommonServiceImpl) environmentRestHandlerImpl := cluster3.NewEnvironmentRestHandlerImpl(environmentServiceImpl, environmentReadServiceImpl, sugaredLogger, userServiceImpl, validate, enforcerImpl, deleteServiceExtendedImpl, k8sServiceImpl, k8sCommonServiceImpl, commonEnforcementUtilImpl) environmentRouterImpl := cluster3.NewEnvironmentRouterImpl(environmentRestHandlerImpl) - genericNoteRepositoryImpl := repository10.NewGenericNoteRepositoryImpl(db, transactionUtilImpl) - genericNoteHistoryRepositoryImpl := repository10.NewGenericNoteHistoryRepositoryImpl(db, transactionUtilImpl) + genericNoteRepositoryImpl := repository11.NewGenericNoteRepositoryImpl(db, transactionUtilImpl) + genericNoteHistoryRepositoryImpl := repository11.NewGenericNoteHistoryRepositoryImpl(db, transactionUtilImpl) genericNoteHistoryServiceImpl := genericNotes.NewGenericNoteHistoryServiceImpl(genericNoteHistoryRepositoryImpl, sugaredLogger) genericNoteServiceImpl := genericNotes.NewGenericNoteServiceImpl(genericNoteRepositoryImpl, genericNoteHistoryServiceImpl, userRepositoryImpl, sugaredLogger) - clusterDescriptionRepositoryImpl := repository5.NewClusterDescriptionRepositoryImpl(db, sugaredLogger) + clusterDescriptionRepositoryImpl := repository6.NewClusterDescriptionRepositoryImpl(db, sugaredLogger) clusterDescriptionServiceImpl := cluster.NewClusterDescriptionServiceImpl(clusterDescriptionRepositoryImpl, userRepositoryImpl, sugaredLogger) clusterRbacServiceImpl := rbac2.NewClusterRbacServiceImpl(environmentServiceImpl, enforcerImpl, enforcerUtilImpl, clusterServiceImplExtended, sugaredLogger, userServiceImpl, clusterReadServiceImpl) clusterRestHandlerImpl := cluster3.NewClusterRestHandlerImpl(clusterServiceImplExtended, genericNoteServiceImpl, clusterDescriptionServiceImpl, sugaredLogger, userServiceImpl, validate, enforcerImpl, deleteServiceExtendedImpl, environmentServiceImpl, clusterRbacServiceImpl) clusterRouterImpl := cluster3.NewClusterRouterImpl(clusterRestHandlerImpl) - gitWebhookRepositoryImpl := repository11.NewGitWebhookRepositoryImpl(db) + gitWebhookRepositoryImpl := repository12.NewGitWebhookRepositoryImpl(db) ciCdConfig, err := types.GetCiCdConfig() if err != nil { return nil, err } - gitProviderRepositoryImpl := repository12.NewGitProviderRepositoryImpl(db, environmentVariables) + gitProviderRepositoryImpl := repository13.NewGitProviderRepositoryImpl(db, environmentVariables) gitProviderReadServiceImpl := read9.NewGitProviderReadService(sugaredLogger, gitProviderRepositoryImpl) commonBaseServiceImpl := commonService.NewCommonBaseServiceImpl(sugaredLogger, environmentVariables, moduleReadServiceImpl) commonServiceImpl := commonService.NewCommonServiceImpl(sugaredLogger, chartRepositoryImpl, envConfigOverrideRepositoryImpl, dockerArtifactStoreRepositoryImpl, attributesRepositoryImpl, environmentRepositoryImpl, appRepositoryImpl, gitOpsConfigReadServiceImpl, gitProviderReadServiceImpl, envConfigOverrideReadServiceImpl, commonBaseServiceImpl, teamReadServiceImpl) @@ -512,8 +517,8 @@ func InitializeApp() (*App, error) { mergeUtil := util.MergeUtil{ Logger: sugaredLogger, } - scopedVariableRepositoryImpl := repository13.NewScopedVariableRepository(db, sugaredLogger, transactionUtilImpl) - devtronResourceSearchableKeyRepositoryImpl := repository14.NewDevtronResourceSearchableKeyRepositoryImpl(sugaredLogger, db) + scopedVariableRepositoryImpl := repository14.NewScopedVariableRepository(db, sugaredLogger, transactionUtilImpl) + devtronResourceSearchableKeyRepositoryImpl := repository15.NewDevtronResourceSearchableKeyRepositoryImpl(sugaredLogger, db) devtronResourceSearchableKeyServiceImpl, err := read10.NewDevtronResourceSearchableKeyServiceImpl(sugaredLogger, devtronResourceSearchableKeyRepositoryImpl) if err != nil { return nil, err @@ -530,9 +535,9 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - variableEntityMappingRepositoryImpl := repository13.NewVariableEntityMappingRepository(sugaredLogger, db, transactionUtilImpl) + variableEntityMappingRepositoryImpl := repository14.NewVariableEntityMappingRepository(sugaredLogger, db, transactionUtilImpl) variableEntityMappingServiceImpl := variables.NewVariableEntityMappingServiceImpl(variableEntityMappingRepositoryImpl, sugaredLogger) - variableSnapshotHistoryRepositoryImpl := repository13.NewVariableSnapshotHistoryRepository(sugaredLogger, db) + variableSnapshotHistoryRepositoryImpl := repository14.NewVariableSnapshotHistoryRepository(sugaredLogger, db) variableSnapshotHistoryServiceImpl := variables.NewVariableSnapshotHistoryServiceImpl(variableSnapshotHistoryRepositoryImpl, sugaredLogger) variableTemplateParserImpl, err := parsers.NewVariableTemplateParserImpl(sugaredLogger) if err != nil { @@ -553,7 +558,7 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - infraConfigRepositoryImpl := repository15.NewInfraProfileRepositoryImpl(db, transactionUtilImpl) + infraConfigRepositoryImpl := repository16.NewInfraProfileRepositoryImpl(db, transactionUtilImpl) pipelineOverrideRepositoryImpl := chartConfig.NewPipelineOverrideRepository(db) utilMergeUtil := &util.MergeUtil{ Logger: sugaredLogger, @@ -586,7 +591,7 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - scanToolMetadataRepositoryImpl := repository16.NewScanToolMetadataRepositoryImpl(db, sugaredLogger) + scanToolMetadataRepositoryImpl := repository17.NewScanToolMetadataRepositoryImpl(db, sugaredLogger) scanToolMetadataServiceImpl := scanTool.NewScanToolMetadataServiceImpl(sugaredLogger, scanToolMetadataRepositoryImpl) moduleServiceImpl := module.NewModuleServiceImpl(sugaredLogger, serverEnvConfigServerEnvConfig, moduleRepositoryImpl, moduleActionAuditLogRepositoryImpl, helmAppServiceImpl, serverDataStoreServerDataStore, serverCacheServiceImpl, moduleCacheServiceImpl, moduleCronServiceImpl, moduleServiceHelperImpl, moduleResourceStatusRepositoryImpl, scanToolMetadataServiceImpl, environmentVariables, moduleEnvConfig) notificationSettingsRepositoryImpl := repository2.NewNotificationSettingsRepositoryImpl(db) @@ -621,11 +626,11 @@ func InitializeApp() (*App, error) { ciTemplateOverrideRepositoryImpl := pipelineConfig.NewCiTemplateOverrideRepositoryImpl(db, sugaredLogger) ciPipelineConfigReadServiceImpl := read14.NewCiPipelineConfigReadServiceImpl(sugaredLogger, ciPipelineRepositoryImpl, ciTemplateOverrideRepositoryImpl) dockerRegistryIpsConfigServiceImpl := dockerRegistry.NewDockerRegistryIpsConfigServiceImpl(sugaredLogger, dockerRegistryIpsConfigRepositoryImpl, k8sServiceImpl, dockerArtifactStoreRepositoryImpl, clusterReadServiceImpl, ciPipelineConfigReadServiceImpl) - appLevelMetricsRepositoryImpl := repository17.NewAppLevelMetricsRepositoryImpl(db, sugaredLogger) - envLevelAppMetricsRepositoryImpl := repository17.NewEnvLevelAppMetricsRepositoryImpl(db, sugaredLogger) + appLevelMetricsRepositoryImpl := repository18.NewAppLevelMetricsRepositoryImpl(db, sugaredLogger) + envLevelAppMetricsRepositoryImpl := repository18.NewEnvLevelAppMetricsRepositoryImpl(db, sugaredLogger) deployedAppMetricsServiceImpl := deployedAppMetrics.NewDeployedAppMetricsServiceImpl(sugaredLogger, appLevelMetricsRepositoryImpl, envLevelAppMetricsRepositoryImpl, chartRefServiceImpl) appListingServiceImpl := app2.NewAppListingServiceImpl(sugaredLogger, appListingRepositoryImpl, appDetailsReadServiceImpl, appRepositoryImpl, appListingViewBuilderImpl, pipelineRepositoryImpl, linkoutsRepositoryImpl, cdWorkflowRepositoryImpl, pipelineOverrideRepositoryImpl, environmentRepositoryImpl, chartRepositoryImpl, ciPipelineRepositoryImpl, dockerRegistryIpsConfigServiceImpl, userRepositoryImpl, deployedAppMetricsServiceImpl, ciArtifactRepositoryImpl, envConfigOverrideReadServiceImpl, ciPipelineConfigReadServiceImpl) - workflowStageRepositoryImpl := repository18.NewWorkflowStageRepositoryImpl(sugaredLogger, db) + workflowStageRepositoryImpl := repository19.NewWorkflowStageRepositoryImpl(sugaredLogger, db) workFlowStageStatusServiceImpl := workflowStatus.NewWorkflowStageFlowStatusServiceImpl(sugaredLogger, workflowStageRepositoryImpl, ciWorkflowRepositoryImpl, cdWorkflowRepositoryImpl, environmentRepositoryImpl, transactionUtilImpl) workflowStatusLatestRepositoryImpl := pipelineConfig.NewWorkflowStatusLatestRepositoryImpl(db, sugaredLogger) workflowStatusLatestServiceImpl := workflowStatusLatest.NewWorkflowStatusLatestServiceImpl(sugaredLogger, workflowStatusLatestRepositoryImpl, ciWorkflowRepositoryImpl, cdWorkflowRepositoryImpl, ciPipelineRepositoryImpl) @@ -644,15 +649,15 @@ func InitializeApp() (*App, error) { ciInfraGetter := ci.NewCiInfraGetter(sugaredLogger, infraConfigServiceImpl, infraConfigAuditServiceImpl) infraProviderImpl := infraProviders.NewInfraProviderImpl(sugaredLogger, infraGetter, ciInfraGetter) serviceImpl := ucid.NewServiceImpl(sugaredLogger, k8sServiceImpl, acdAuthConfig) - workflowConfigSnapshotRepositoryImpl := repository19.NewWorkflowConfigSnapshotRepositoryImpl(db, sugaredLogger, transactionUtilImpl) + workflowConfigSnapshotRepositoryImpl := repository20.NewWorkflowConfigSnapshotRepositoryImpl(db, sugaredLogger, transactionUtilImpl) workflowTriggerAuditServiceImpl := service3.NewWorkflowTriggerAuditServiceImpl(sugaredLogger, workflowConfigSnapshotRepositoryImpl, ciCdConfig, dockerRegistryConfigImpl, transactionUtilImpl) triggerAuditHookImpl := hook.NewTriggerAuditHookImpl(sugaredLogger, workflowTriggerAuditServiceImpl) workflowServiceImpl, err := executor.NewWorkflowServiceImpl(sugaredLogger, environmentRepositoryImpl, ciCdConfig, configReadServiceImpl, globalCMCSServiceImpl, argoWorkflowExecutorImpl, systemWorkflowExecutorImpl, k8sCommonServiceImpl, infraProviderImpl, serviceImpl, k8sServiceImpl, triggerAuditHookImpl, infraConfigAuditServiceImpl) if err != nil { return nil, err } - pipelineStageRepositoryImpl := repository20.NewPipelineStageRepository(sugaredLogger, db) - globalPluginRepositoryImpl := repository21.NewGlobalPluginRepository(sugaredLogger, db) + pipelineStageRepositoryImpl := repository21.NewPipelineStageRepository(sugaredLogger, db) + globalPluginRepositoryImpl := repository22.NewGlobalPluginRepository(sugaredLogger, db) globalPluginServiceImpl := plugin.NewGlobalPluginService(sugaredLogger, globalPluginRepositoryImpl, pipelineStageRepositoryImpl, userServiceImpl) pipelineStageServiceImpl := pipeline.NewPipelineStageService(sugaredLogger, pipelineStageRepositoryImpl, globalPluginRepositoryImpl, pipelineRepositoryImpl, scopedVariableManagerImpl, globalPluginServiceImpl) ciTemplateRepositoryImpl := pipelineConfig.NewCiTemplateRepositoryImpl(db, sugaredLogger) @@ -662,7 +667,7 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - materialRepositoryImpl := repository22.NewMaterialRepositoryImpl(db) + materialRepositoryImpl := repository23.NewMaterialRepositoryImpl(db) gitMaterialReadServiceImpl := read15.NewGitMaterialReadServiceImpl(sugaredLogger, materialRepositoryImpl) appCrudOperationServiceImpl := app2.NewAppCrudOperationServiceImpl(appLabelRepositoryImpl, sugaredLogger, appRepositoryImpl, userRepositoryImpl, installedAppRepositoryImpl, genericNoteServiceImpl, installedAppDBServiceImpl, crudOperationServiceConfig, dbMigrationServiceImpl, gitMaterialReadServiceImpl) imageTagRepositoryImpl := repository2.NewImageTagRepository(db, sugaredLogger) @@ -675,20 +680,20 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - prePostCdScriptHistoryRepositoryImpl := repository23.NewPrePostCdScriptHistoryRepositoryImpl(sugaredLogger, db) - configMapHistoryRepositoryImpl := repository23.NewConfigMapHistoryRepositoryImpl(sugaredLogger, db, transactionUtilImpl) + prePostCdScriptHistoryRepositoryImpl := repository24.NewPrePostCdScriptHistoryRepositoryImpl(sugaredLogger, db) + configMapHistoryRepositoryImpl := repository24.NewConfigMapHistoryRepositoryImpl(sugaredLogger, db, transactionUtilImpl) configMapHistoryServiceImpl := configMapAndSecret.NewConfigMapHistoryServiceImpl(sugaredLogger, configMapHistoryRepositoryImpl, pipelineRepositoryImpl, configMapRepositoryImpl, userServiceImpl, scopedVariableCMCSManagerImpl) prePostCdScriptHistoryServiceImpl := history.NewPrePostCdScriptHistoryServiceImpl(sugaredLogger, prePostCdScriptHistoryRepositoryImpl, configMapRepositoryImpl, configMapHistoryServiceImpl) - gitMaterialHistoryRepositoryImpl := repository23.NewGitMaterialHistoryRepositoyImpl(db) + gitMaterialHistoryRepositoryImpl := repository24.NewGitMaterialHistoryRepositoyImpl(db) gitMaterialHistoryServiceImpl := history.NewGitMaterialHistoryServiceImpl(gitMaterialHistoryRepositoryImpl, sugaredLogger) - ciPipelineHistoryRepositoryImpl := repository23.NewCiPipelineHistoryRepositoryImpl(db, sugaredLogger) + ciPipelineHistoryRepositoryImpl := repository24.NewCiPipelineHistoryRepositoryImpl(db, sugaredLogger) ciPipelineHistoryServiceImpl := history.NewCiPipelineHistoryServiceImpl(ciPipelineHistoryRepositoryImpl, sugaredLogger, ciPipelineRepositoryImpl) ciBuildConfigRepositoryImpl := pipelineConfig.NewCiBuildConfigRepositoryImpl(db, sugaredLogger) ciBuildConfigServiceImpl := pipeline.NewCiBuildConfigServiceImpl(sugaredLogger, ciBuildConfigRepositoryImpl) ciTemplateServiceImpl := pipeline.NewCiTemplateServiceImpl(sugaredLogger, ciBuildConfigServiceImpl, ciTemplateRepositoryImpl, ciTemplateOverrideRepositoryImpl) pipelineConfigRepositoryImpl := chartConfig.NewPipelineConfigRepository(db) configMapServiceImpl := pipeline.NewConfigMapServiceImpl(chartRepositoryImpl, sugaredLogger, chartRepoRepositoryImpl, mergeUtil, pipelineConfigRepositoryImpl, configMapRepositoryImpl, commonServiceImpl, appRepositoryImpl, configMapHistoryServiceImpl, environmentRepositoryImpl, scopedVariableCMCSManagerImpl) - deploymentTemplateHistoryRepositoryImpl := repository23.NewDeploymentTemplateHistoryRepositoryImpl(sugaredLogger, db) + deploymentTemplateHistoryRepositoryImpl := repository24.NewDeploymentTemplateHistoryRepositoryImpl(sugaredLogger, db) deploymentTemplateHistoryServiceImpl := deploymentTemplate.NewDeploymentTemplateHistoryServiceImpl(sugaredLogger, deploymentTemplateHistoryRepositoryImpl, pipelineRepositoryImpl, chartRepositoryImpl, userServiceImpl, cdWorkflowRepositoryImpl, scopedVariableManagerImpl, deployedAppMetricsServiceImpl, chartRefServiceImpl) chartReadServiceImpl := read16.NewChartReadServiceImpl(sugaredLogger, chartRepositoryImpl, deploymentConfigServiceImpl, deployedAppMetricsServiceImpl, gitOpsConfigReadServiceImpl, chartRefReadServiceImpl) chartServiceImpl := chart.NewChartServiceImpl(chartRepositoryImpl, sugaredLogger, chartTemplateServiceImpl, chartRepoRepositoryImpl, appRepositoryImpl, mergeUtil, envConfigOverrideRepositoryImpl, pipelineConfigRepositoryImpl, environmentRepositoryImpl, deploymentTemplateHistoryServiceImpl, scopedVariableManagerImpl, deployedAppMetricsServiceImpl, chartRefServiceImpl, gitOpsConfigReadServiceImpl, deploymentConfigServiceImpl, envConfigOverrideReadServiceImpl, chartReadServiceImpl) @@ -707,7 +712,7 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - ciTemplateHistoryRepositoryImpl := repository23.NewCiTemplateHistoryRepositoryImpl(db, sugaredLogger) + ciTemplateHistoryRepositoryImpl := repository24.NewCiTemplateHistoryRepositoryImpl(db, sugaredLogger) ciTemplateHistoryServiceImpl := history.NewCiTemplateHistoryServiceImpl(ciTemplateHistoryRepositoryImpl, sugaredLogger) resourceGroupRepositoryImpl := resourceGroup.NewResourceGroupRepositoryImpl(db) resourceGroupMappingRepositoryImpl := resourceGroup.NewResourceGroupMappingRepositoryImpl(db) @@ -715,14 +720,14 @@ func InitializeApp() (*App, error) { buildPipelineSwitchServiceImpl := pipeline.NewBuildPipelineSwitchServiceImpl(sugaredLogger, ciPipelineConfigReadServiceImpl, ciPipelineRepositoryImpl, ciCdPipelineOrchestratorImpl, pipelineRepositoryImpl, ciWorkflowRepositoryImpl, appWorkflowRepositoryImpl, ciPipelineHistoryServiceImpl, ciTemplateOverrideRepositoryImpl, ciPipelineMaterialRepositoryImpl) ciPipelineConfigServiceImpl := pipeline.NewCiPipelineConfigServiceImpl(sugaredLogger, ciCdPipelineOrchestratorImpl, dockerArtifactStoreRepositoryImpl, gitMaterialReadServiceImpl, appRepositoryImpl, pipelineRepositoryImpl, ciPipelineConfigReadServiceImpl, ciPipelineRepositoryImpl, ecrConfig, appWorkflowRepositoryImpl, ciCdConfig, attributesServiceImpl, pipelineStageServiceImpl, ciPipelineMaterialRepositoryImpl, ciTemplateServiceImpl, ciTemplateReadServiceImpl, ciTemplateOverrideRepositoryImpl, ciTemplateHistoryServiceImpl, enforcerUtilImpl, ciWorkflowRepositoryImpl, resourceGroupServiceImpl, customTagServiceImpl, cdWorkflowRepositoryImpl, buildPipelineSwitchServiceImpl, pipelineStageRepositoryImpl, globalPluginRepositoryImpl, appListingServiceImpl) ciMaterialConfigServiceImpl := pipeline.NewCiMaterialConfigServiceImpl(sugaredLogger, materialRepositoryImpl, ciTemplateReadServiceImpl, ciCdPipelineOrchestratorImpl, ciPipelineRepositoryImpl, gitMaterialHistoryServiceImpl, pipelineRepositoryImpl, ciPipelineMaterialRepositoryImpl, transactionUtilImpl, gitMaterialReadServiceImpl) - imageTaggingRepositoryImpl := repository24.NewImageTaggingRepositoryImpl(db, transactionUtilImpl) + imageTaggingRepositoryImpl := repository25.NewImageTaggingRepositoryImpl(db, transactionUtilImpl) imageTaggingReadServiceImpl, err := read17.NewImageTaggingReadServiceImpl(imageTaggingRepositoryImpl, sugaredLogger) if err != nil { return nil, err } imageTaggingServiceImpl := imageTagging.NewImageTaggingServiceImpl(imageTaggingRepositoryImpl, imageTaggingReadServiceImpl, ciPipelineRepositoryImpl, pipelineRepositoryImpl, environmentRepositoryImpl, sugaredLogger) deploymentGroupRepositoryImpl := repository2.NewDeploymentGroupRepositoryImpl(sugaredLogger, db) - pipelineStrategyHistoryRepositoryImpl := repository23.NewPipelineStrategyHistoryRepositoryImpl(sugaredLogger, db) + pipelineStrategyHistoryRepositoryImpl := repository24.NewPipelineStrategyHistoryRepositoryImpl(sugaredLogger, db) pipelineStrategyHistoryServiceImpl := history.NewPipelineStrategyHistoryServiceImpl(sugaredLogger, pipelineStrategyHistoryRepositoryImpl, userServiceImpl) propertiesConfigServiceImpl := pipeline.NewPropertiesConfigServiceImpl(sugaredLogger, envConfigOverrideRepositoryImpl, chartRepositoryImpl, environmentRepositoryImpl, deploymentTemplateHistoryServiceImpl, scopedVariableManagerImpl, deployedAppMetricsServiceImpl, envConfigOverrideReadServiceImpl, deploymentConfigServiceImpl, chartServiceImpl) installedAppDBExtendedServiceImpl := FullMode.NewInstalledAppDBExtendedServiceImpl(installedAppDBServiceImpl, appStatusServiceImpl, gitOpsConfigReadServiceImpl) @@ -765,13 +770,13 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - cvePolicyRepositoryImpl := repository25.NewPolicyRepositoryImpl(db, sugaredLogger) - imageScanResultRepositoryImpl := repository25.NewImageScanResultRepositoryImpl(db, sugaredLogger) - imageScanDeployInfoRepositoryImpl := repository25.NewImageScanDeployInfoRepositoryImpl(db, sugaredLogger) - imageScanObjectMetaRepositoryImpl := repository25.NewImageScanObjectMetaRepositoryImpl(db, sugaredLogger) - imageScanHistoryRepositoryImpl := repository25.NewImageScanHistoryRepositoryImpl(db, sugaredLogger) + cvePolicyRepositoryImpl := repository26.NewPolicyRepositoryImpl(db, sugaredLogger) + imageScanResultRepositoryImpl := repository26.NewImageScanResultRepositoryImpl(db, sugaredLogger) + imageScanDeployInfoRepositoryImpl := repository26.NewImageScanDeployInfoRepositoryImpl(db, sugaredLogger) + imageScanObjectMetaRepositoryImpl := repository26.NewImageScanObjectMetaRepositoryImpl(db, sugaredLogger) + imageScanHistoryRepositoryImpl := repository26.NewImageScanHistoryRepositoryImpl(db, sugaredLogger) imageScanHistoryReadServiceImpl := read19.NewImageScanHistoryReadService(sugaredLogger, imageScanHistoryRepositoryImpl) - cveStoreRepositoryImpl := repository25.NewCveStoreRepositoryImpl(db, sugaredLogger) + cveStoreRepositoryImpl := repository26.NewCveStoreRepositoryImpl(db, sugaredLogger) policyServiceImpl := imageScanning.NewPolicyServiceImpl(environmentServiceImpl, sugaredLogger, appRepositoryImpl, pipelineOverrideRepositoryImpl, cvePolicyRepositoryImpl, clusterServiceImplExtended, pipelineRepositoryImpl, imageScanResultRepositoryImpl, imageScanDeployInfoRepositoryImpl, imageScanObjectMetaRepositoryImpl, httpClient, ciArtifactRepositoryImpl, ciCdConfig, imageScanHistoryReadServiceImpl, cveStoreRepositoryImpl, ciTemplateRepositoryImpl, clusterReadServiceImpl, transactionUtilImpl) imageScanResultReadServiceImpl := read19.NewImageScanResultReadServiceImpl(sugaredLogger, imageScanResultRepositoryImpl) draftAwareConfigServiceImpl := draftAwareConfigService.NewDraftAwareResourceServiceImpl(sugaredLogger, configMapServiceImpl, chartServiceImpl, propertiesConfigServiceImpl) @@ -779,12 +784,12 @@ func InitializeApp() (*App, error) { manifestCreationServiceImpl := manifest.NewManifestCreationServiceImpl(sugaredLogger, dockerRegistryIpsConfigServiceImpl, chartRefServiceImpl, scopedVariableCMCSManagerImpl, k8sCommonServiceImpl, deployedAppMetricsServiceImpl, imageDigestPolicyServiceImpl, utilMergeUtil, appCrudOperationServiceImpl, deploymentTemplateServiceImpl, argoClientWrapperServiceImpl, configMapHistoryRepositoryImpl, configMapRepositoryImpl, chartRepositoryImpl, envConfigOverrideRepositoryImpl, environmentRepositoryImpl, pipelineRepositoryImpl, ciArtifactRepositoryImpl, pipelineOverrideRepositoryImpl, pipelineStrategyHistoryRepositoryImpl, pipelineConfigRepositoryImpl, deploymentTemplateHistoryRepositoryImpl, deploymentConfigServiceImpl, envConfigOverrideReadServiceImpl) configMapHistoryReadServiceImpl := read20.NewConfigMapHistoryReadService(sugaredLogger, configMapHistoryRepositoryImpl, scopedVariableCMCSManagerImpl) deployedConfigurationHistoryServiceImpl := history.NewDeployedConfigurationHistoryServiceImpl(sugaredLogger, userServiceImpl, deploymentTemplateHistoryServiceImpl, pipelineStrategyHistoryServiceImpl, configMapHistoryServiceImpl, cdWorkflowRepositoryImpl, scopedVariableCMCSManagerImpl, deploymentTemplateHistoryReadServiceImpl, configMapHistoryReadServiceImpl) - userDeploymentRequestRepositoryImpl := repository26.NewUserDeploymentRequestRepositoryImpl(db, transactionUtilImpl) + userDeploymentRequestRepositoryImpl := repository27.NewUserDeploymentRequestRepositoryImpl(db, transactionUtilImpl) userDeploymentRequestServiceImpl := service4.NewUserDeploymentRequestServiceImpl(sugaredLogger, userDeploymentRequestRepositoryImpl) imageScanDeployInfoReadServiceImpl := read19.NewImageScanDeployInfoReadService(sugaredLogger, imageScanDeployInfoRepositoryImpl) imageScanDeployInfoServiceImpl := imageScanning.NewImageScanDeployInfoService(sugaredLogger, imageScanDeployInfoRepositoryImpl) - manifestPushConfigRepositoryImpl := repository20.NewManifestPushConfigRepository(sugaredLogger, db) - scanToolExecutionHistoryMappingRepositoryImpl := repository25.NewScanToolExecutionHistoryMappingRepositoryImpl(db, sugaredLogger) + manifestPushConfigRepositoryImpl := repository21.NewManifestPushConfigRepository(sugaredLogger, db) + scanToolExecutionHistoryMappingRepositoryImpl := repository26.NewScanToolExecutionHistoryMappingRepositoryImpl(db, sugaredLogger) cdWorkflowReadServiceImpl := read18.NewCdWorkflowReadServiceImpl(sugaredLogger, cdWorkflowRepositoryImpl) imageScanServiceImpl := imageScanning.NewImageScanServiceImpl(sugaredLogger, imageScanHistoryRepositoryImpl, imageScanResultRepositoryImpl, imageScanObjectMetaRepositoryImpl, cveStoreRepositoryImpl, imageScanDeployInfoRepositoryImpl, userServiceImpl, appRepositoryImpl, environmentServiceImpl, ciArtifactRepositoryImpl, policyServiceImpl, pipelineRepositoryImpl, ciPipelineRepositoryImpl, scanToolMetadataRepositoryImpl, scanToolExecutionHistoryMappingRepositoryImpl, cvePolicyRepositoryImpl, cdWorkflowReadServiceImpl) devtronAppsHandlerServiceImpl, err := devtronApps.NewHandlerServiceImpl(sugaredLogger, cdWorkflowCommonServiceImpl, gitOpsManifestPushServiceImpl, gitOpsConfigReadServiceImpl, argoK8sClientImpl, acdConfig, argoClientWrapperServiceImpl, pipelineStatusTimelineServiceImpl, chartTemplateServiceImpl, workflowEventPublishServiceImpl, manifestCreationServiceImpl, deployedConfigurationHistoryServiceImpl, pipelineStageServiceImpl, globalPluginServiceImpl, customTagServiceImpl, pluginInputVariableParserImpl, prePostCdScriptHistoryServiceImpl, scopedVariableCMCSManagerImpl, imageDigestPolicyServiceImpl, userServiceImpl, helmAppServiceImpl, enforcerUtilImpl, userDeploymentRequestServiceImpl, helmAppClientImpl, eventSimpleFactoryImpl, eventRESTClientImpl, environmentVariables, appRepositoryImpl, ciPipelineMaterialRepositoryImpl, imageScanHistoryReadServiceImpl, imageScanDeployInfoReadServiceImpl, imageScanDeployInfoServiceImpl, pipelineRepositoryImpl, pipelineOverrideRepositoryImpl, manifestPushConfigRepositoryImpl, chartRepositoryImpl, environmentRepositoryImpl, cdWorkflowRepositoryImpl, ciWorkflowRepositoryImpl, ciArtifactRepositoryImpl, ciTemplateReadServiceImpl, gitMaterialReadServiceImpl, appLabelRepositoryImpl, ciPipelineRepositoryImpl, appWorkflowRepositoryImpl, dockerArtifactStoreRepositoryImpl, imageScanServiceImpl, k8sServiceImpl, transactionUtilImpl, deploymentConfigServiceImpl, ciCdPipelineOrchestratorImpl, gitOperationServiceImpl, attributesServiceImpl, clusterRepositoryImpl, cdWorkflowRunnerServiceImpl, clusterServiceImplExtended, ciLogServiceImpl, workflowServiceImpl, blobStorageConfigServiceImpl, deploymentEventHandlerImpl, runnable, workflowTriggerAuditServiceImpl, deploymentServiceImpl, workflowStatusLatestServiceImpl) @@ -800,7 +805,7 @@ func InitializeApp() (*App, error) { webhookRouterImpl := router.NewWebhookRouterImpl(gitWebhookRestHandlerImpl, pipelineConfigRestHandlerImpl, externalCiRestHandlerImpl, pubSubClientRestHandlerImpl) userAuthHandlerImpl := user2.NewUserAuthHandlerImpl(userAuthServiceImpl, validate, sugaredLogger, enforcerImpl) selfRegistrationRolesRepositoryImpl := repository4.NewSelfRegistrationRolesRepositoryImpl(db, sugaredLogger) - userSelfRegistrationServiceImpl := user.NewUserSelfRegistrationServiceImpl(sugaredLogger, selfRegistrationRolesRepositoryImpl, userServiceImpl) + userSelfRegistrationServiceImpl := user.NewUserSelfRegistrationServiceImpl(sugaredLogger, selfRegistrationRolesRepositoryImpl, userServiceImpl, globalAuthorisationConfigServiceImpl) userAuthOidcHelperImpl, err := authentication.NewUserAuthOidcHelperImpl(sugaredLogger, userSelfRegistrationServiceImpl, dexConfig, settings, sessionManager) if err != nil { return nil, err @@ -810,7 +815,7 @@ func InitializeApp() (*App, error) { deleteServiceFullModeImpl := delete2.NewDeleteServiceFullModeImpl(sugaredLogger, gitMaterialReadServiceImpl, gitRegistryConfigImpl, ciTemplateRepositoryImpl, dockerRegistryConfigImpl, dockerArtifactStoreRepositoryImpl) gitProviderRestHandlerImpl := restHandler.NewGitProviderRestHandlerImpl(dockerRegistryConfigImpl, sugaredLogger, gitRegistryConfigImpl, userServiceImpl, validate, enforcerImpl, teamServiceImpl, deleteServiceFullModeImpl, gitProviderReadServiceImpl) gitProviderRouterImpl := router.NewGitProviderRouterImpl(gitProviderRestHandlerImpl) - gitHostRepositoryImpl := repository27.NewGitHostRepositoryImpl(db) + gitHostRepositoryImpl := repository28.NewGitHostRepositoryImpl(db) gitHostConfigImpl := gitHost.NewGitHostConfigImpl(gitHostRepositoryImpl, sugaredLogger) gitHostReadServiceImpl := read21.NewGitHostReadServiceImpl(sugaredLogger, gitHostRepositoryImpl, attributesServiceImpl) gitHostRestHandlerImpl := restHandler.NewGitHostRestHandlerImpl(sugaredLogger, gitHostConfigImpl, userServiceImpl, validate, enforcerImpl, clientImpl, gitProviderReadServiceImpl, gitHostReadServiceImpl) @@ -838,9 +843,9 @@ func InitializeApp() (*App, error) { chartRefRouterImpl := router.NewChartRefRouterImpl(chartRefRestHandlerImpl) configMapRestHandlerImpl := restHandler.NewConfigMapRestHandlerImpl(pipelineBuilderImpl, sugaredLogger, chartServiceImpl, userServiceImpl, teamServiceImpl, enforcerImpl, pipelineRepositoryImpl, enforcerUtilImpl, configMapServiceImpl, draftAwareConfigServiceImpl) configMapRouterImpl := router.NewConfigMapRouterImpl(configMapRestHandlerImpl) - k8sResourceHistoryRepositoryImpl := repository28.NewK8sResourceHistoryRepositoryImpl(db, sugaredLogger) + k8sResourceHistoryRepositoryImpl := repository29.NewK8sResourceHistoryRepositoryImpl(db, sugaredLogger) k8sResourceHistoryServiceImpl := kubernetesResourceAuditLogs.Newk8sResourceHistoryServiceImpl(k8sResourceHistoryRepositoryImpl, sugaredLogger, appRepositoryImpl, environmentRepositoryImpl) - ephemeralContainersRepositoryImpl := repository5.NewEphemeralContainersRepositoryImpl(db, transactionUtilImpl) + ephemeralContainersRepositoryImpl := repository6.NewEphemeralContainersRepositoryImpl(db, transactionUtilImpl) ephemeralContainerServiceImpl := cluster.NewEphemeralContainerServiceImpl(ephemeralContainersRepositoryImpl, sugaredLogger) terminalSessionHandlerImpl := terminal.NewTerminalSessionHandlerImpl(environmentServiceImpl, sugaredLogger, k8sServiceImpl, ephemeralContainerServiceImpl, argoApplicationConfigServiceImpl, clusterReadServiceImpl, runnable) k8sApplicationServiceImpl, err := application2.NewK8sApplicationServiceImpl(sugaredLogger, clusterServiceImplExtended, pumpImpl, helmAppServiceImpl, k8sServiceImpl, acdAuthConfig, k8sResourceHistoryServiceImpl, k8sCommonServiceImpl, terminalSessionHandlerImpl, ephemeralContainerServiceImpl, ephemeralContainersRepositoryImpl, fluxApplicationServiceImpl, clusterReadServiceImpl) @@ -851,9 +856,9 @@ func InitializeApp() (*App, error) { argoApplicationReadServiceImpl := read22.NewArgoApplicationReadServiceImpl(sugaredLogger, clusterRepositoryImpl, k8sServiceImpl, helmAppClientImpl, helmAppServiceImpl) argoApplicationServiceExtendedImpl := argoApplication.NewArgoApplicationServiceExtendedServiceImpl(acdAuthConfig, argoApplicationServiceImpl, argoClientWrapperServiceImpl, argoApplicationReadServiceImpl, clusterServiceImplExtended, runnable) installedAppResourceServiceImpl := resource.NewInstalledAppResourceServiceImpl(sugaredLogger, installedAppRepositoryImpl, appStoreApplicationVersionRepositoryImpl, argoClientWrapperServiceImpl, acdAuthConfig, installedAppVersionHistoryRepositoryImpl, helmAppServiceImpl, helmAppReadServiceImpl, appStatusServiceImpl, k8sCommonServiceImpl, k8sApplicationServiceImpl, k8sServiceImpl, deploymentConfigServiceImpl, ociRegistryConfigRepositoryImpl, argoApplicationServiceExtendedImpl, fluxApplicationServiceImpl) - chartGroupEntriesRepositoryImpl := repository29.NewChartGroupEntriesRepositoryImpl(db, sugaredLogger) - chartGroupReposotoryImpl := repository29.NewChartGroupReposotoryImpl(db, sugaredLogger) - chartGroupDeploymentRepositoryImpl := repository29.NewChartGroupDeploymentRepositoryImpl(db, sugaredLogger) + chartGroupEntriesRepositoryImpl := repository30.NewChartGroupEntriesRepositoryImpl(db, sugaredLogger) + chartGroupReposotoryImpl := repository30.NewChartGroupReposotoryImpl(db, sugaredLogger) + chartGroupDeploymentRepositoryImpl := repository30.NewChartGroupDeploymentRepositoryImpl(db, sugaredLogger) appStoreVersionValuesRepositoryImpl := appStoreValuesRepository.NewAppStoreVersionValuesRepositoryImpl(sugaredLogger, db) appStoreRepositoryImpl := appStoreDiscoverRepository.NewAppStoreRepositoryImpl(sugaredLogger, db) clusterInstalledAppsRepositoryImpl := repository3.NewClusterInstalledAppsRepositoryImpl(db, sugaredLogger) @@ -948,7 +953,7 @@ func InitializeApp() (*App, error) { return nil, err } ssoLoginRepositoryImpl := sso.NewSSOLoginRepositoryImpl(db, sugaredLogger) - ssoLoginServiceImpl := sso.NewSSOLoginServiceImpl(sugaredLogger, ssoLoginRepositoryImpl, k8sServiceImpl, environmentVariables, userAuthOidcHelperImpl) + ssoLoginServiceImpl := sso.NewSSOLoginServiceImpl(sugaredLogger, ssoLoginRepositoryImpl, k8sServiceImpl, environmentVariables, userAuthOidcHelperImpl, globalAuthorisationConfigServiceImpl) ssoLoginRestHandlerImpl := sso2.NewSsoLoginRestHandlerImpl(validate, sugaredLogger, enforcerImpl, userServiceImpl, ssoLoginServiceImpl) ssoLoginRouterImpl := sso2.NewSsoLoginRouterImpl(ssoLoginRestHandlerImpl) posthogClient, err := telemetry.NewPosthogClient(sugaredLogger) @@ -962,7 +967,7 @@ func InitializeApp() (*App, error) { } telemetryRestHandlerImpl := restHandler.NewTelemetryRestHandlerImpl(sugaredLogger, telemetryEventClientImplExtended, enforcerImpl, userServiceImpl) telemetryRouterImpl := router.NewTelemetryRouterImpl(sugaredLogger, telemetryRestHandlerImpl) - bulkEditRepositoryImpl := repository30.NewBulkEditRepository(db, sugaredLogger) + bulkEditRepositoryImpl := repository31.NewBulkEditRepository(db, sugaredLogger) deployedAppServiceImpl := deployedApp.NewDeployedAppServiceImpl(sugaredLogger, k8sCommonServiceImpl, devtronAppsHandlerServiceImpl, environmentRepositoryImpl, pipelineRepositoryImpl, cdWorkflowRepositoryImpl) bulkUpdateServiceEntImpl := service8.NewBulkUpdateServiceEntImpl() bulkUpdateServiceImpl := service8.NewBulkUpdateServiceImpl(bulkEditRepositoryImpl, sugaredLogger, environmentRepositoryImpl, pipelineRepositoryImpl, appRepositoryImpl, deploymentTemplateHistoryServiceImpl, configMapHistoryServiceImpl, pipelineBuilderImpl, enforcerUtilImpl, ciHandlerImpl, ciPipelineRepositoryImpl, appWorkflowRepositoryImpl, appWorkflowServiceImpl, scopedVariableManagerImpl, deployedAppMetricsServiceImpl, chartRefServiceImpl, deployedAppServiceImpl, cdPipelineEventPublishServiceImpl, handlerServiceImpl, bulkUpdateServiceEntImpl) @@ -987,7 +992,7 @@ func InitializeApp() (*App, error) { pipelineTriggerRouterImpl := trigger3.NewPipelineTriggerRouter(pipelineTriggerRestHandlerImpl, sseSSE) webhookDataRestHandlerImpl := webhook.NewWebhookDataRestHandlerImpl(sugaredLogger, userServiceImpl, ciPipelineMaterialRepositoryImpl, enforcerUtilImpl, enforcerImpl, clientImpl, webhookEventDataConfigImpl) pipelineConfigRouterImpl := configure2.NewPipelineRouterImpl(pipelineConfigRestHandlerImpl, webhookDataRestHandlerImpl) - prePostCiScriptHistoryRepositoryImpl := repository23.NewPrePostCiScriptHistoryRepositoryImpl(sugaredLogger, db) + prePostCiScriptHistoryRepositoryImpl := repository24.NewPrePostCiScriptHistoryRepositoryImpl(sugaredLogger, db) prePostCiScriptHistoryServiceImpl := history.NewPrePostCiScriptHistoryServiceImpl(sugaredLogger, prePostCiScriptHistoryRepositoryImpl) pipelineHistoryRestHandlerImpl := history2.NewPipelineHistoryRestHandlerImpl(sugaredLogger, userServiceImpl, enforcerImpl, pipelineStrategyHistoryServiceImpl, deploymentTemplateHistoryServiceImpl, configMapHistoryServiceImpl, prePostCiScriptHistoryServiceImpl, prePostCdScriptHistoryServiceImpl, enforcerUtilImpl, deployedConfigurationHistoryServiceImpl) pipelineHistoryRouterImpl := history3.NewPipelineHistoryRouterImpl(pipelineHistoryRestHandlerImpl) From 634eb598068f18b06406b1196dfeefcedb40c453 Mon Sep 17 00:00:00 2001 From: satya_prakash <155617493+SATYAsasini@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:22:15 +0530 Subject: [PATCH 03/19] fix: global auth apis wire register for ea mode (#6929) * fix: global auth apis wire register for ea mode * fix: linting --- cmd/external-app/router.go | 115 ++++++++++++++++++++----------------- cmd/external-app/wire.go | 2 + 2 files changed, 63 insertions(+), 54 deletions(-) diff --git a/cmd/external-app/router.go b/cmd/external-app/router.go index 01c0af8546..f5227fce56 100644 --- a/cmd/external-app/router.go +++ b/cmd/external-app/router.go @@ -24,6 +24,7 @@ import ( appStoreDiscover "github.com/devtron-labs/devtron/api/appStore/discover" appStoreValues "github.com/devtron-labs/devtron/api/appStore/values" "github.com/devtron-labs/devtron/api/argoApplication" + globalConfigAPI "github.com/devtron-labs/devtron/api/auth/authorisation/globalConfig" "github.com/devtron-labs/devtron/api/auth/sso" "github.com/devtron-labs/devtron/api/auth/user" "github.com/devtron-labs/devtron/api/chartRepo" @@ -70,24 +71,25 @@ type MuxRouter struct { chartProviderRouter chartProvider.ChartProviderRouter dockerRegRouter router.DockerRegRouter - dashboardTelemetryRouter dashboardEvent.DashboardTelemetryRouter - commonDeploymentRouter appStoreDeployment.CommonDeploymentRouter - externalLinksRouter externalLink.ExternalLinkRouter - moduleRouter module.ModuleRouter - serverRouter server.ServerRouter - apiTokenRouter apiToken.ApiTokenRouter - k8sCapacityRouter capacity.K8sCapacityRouter - webhookHelmRouter webhookHelm.WebhookHelmRouter - userAttributesRouter router.UserAttributesRouter - telemetryRouter router.TelemetryRouter - userTerminalAccessRouter terminal.UserTerminalAccessRouter - attributesRouter router.AttributesRouter - appRouter app.AppRouterEAMode - rbacRoleRouter user.RbacRoleRouter - argoApplicationRouter argoApplication.ArgoApplicationRouter - fluxApplicationRouter fluxApplication.FluxApplicationRouter - userResourceRouter userResource.Router - infraOverviewRouter router.InfraOverviewRouter + dashboardTelemetryRouter dashboardEvent.DashboardTelemetryRouter + commonDeploymentRouter appStoreDeployment.CommonDeploymentRouter + externalLinksRouter externalLink.ExternalLinkRouter + moduleRouter module.ModuleRouter + serverRouter server.ServerRouter + apiTokenRouter apiToken.ApiTokenRouter + k8sCapacityRouter capacity.K8sCapacityRouter + webhookHelmRouter webhookHelm.WebhookHelmRouter + userAttributesRouter router.UserAttributesRouter + telemetryRouter router.TelemetryRouter + userTerminalAccessRouter terminal.UserTerminalAccessRouter + attributesRouter router.AttributesRouter + appRouter app.AppRouterEAMode + rbacRoleRouter user.RbacRoleRouter + argoApplicationRouter argoApplication.ArgoApplicationRouter + fluxApplicationRouter fluxApplication.FluxApplicationRouter + userResourceRouter userResource.Router + infraOverviewRouter router.InfraOverviewRouter + globalAuthorisationConfigRouter globalConfigAPI.AuthorisationConfigRouter } func NewMuxRouter( @@ -123,44 +125,46 @@ func NewMuxRouter( rbacRoleRouter user.RbacRoleRouter, argoApplicationRouter argoApplication.ArgoApplicationRouter, fluxApplicationRouter fluxApplication.FluxApplicationRouter, userResourceRouter userResource.Router, infraOverviewRouter router.InfraOverviewRouter, + globalAuthorisationConfigRouter globalConfigAPI.AuthorisationConfigRouter, ) *MuxRouter { r := &MuxRouter{ - Router: mux.NewRouter(), - logger: logger, - ssoLoginRouter: ssoLoginRouter, - teamRouter: teamRouter, - UserAuthRouter: UserAuthRouter, - userRouter: userRouter, - commonRouter: commonRouter, - clusterRouter: clusterRouter, - dashboardRouter: dashboardRouter, - helmAppRouter: helmAppRouter, - environmentRouter: environmentRouter, - k8sApplicationRouter: k8sApplicationRouter, - chartRepositoryRouter: chartRepositoryRouter, - appStoreDiscoverRouter: appStoreDiscoverRouter, - appStoreValuesRouter: appStoreValuesRouter, - appStoreDeploymentRouter: appStoreDeploymentRouter, - chartProviderRouter: chartProviderRouter, - dockerRegRouter: dockerRegRouter, - dashboardTelemetryRouter: dashboardTelemetryRouter, - commonDeploymentRouter: commonDeploymentRouter, - externalLinksRouter: externalLinkRouter, - moduleRouter: moduleRouter, - serverRouter: serverRouter, - apiTokenRouter: apiTokenRouter, - k8sCapacityRouter: k8sCapacityRouter, - webhookHelmRouter: webhookHelmRouter, - userAttributesRouter: userAttributesRouter, - telemetryRouter: telemetryRouter, - userTerminalAccessRouter: userTerminalAccessRouter, - attributesRouter: attributesRouter, - appRouter: appRouter, - rbacRoleRouter: rbacRoleRouter, - argoApplicationRouter: argoApplicationRouter, - fluxApplicationRouter: fluxApplicationRouter, - userResourceRouter: userResourceRouter, - infraOverviewRouter: infraOverviewRouter, + Router: mux.NewRouter(), + logger: logger, + ssoLoginRouter: ssoLoginRouter, + teamRouter: teamRouter, + UserAuthRouter: UserAuthRouter, + userRouter: userRouter, + commonRouter: commonRouter, + clusterRouter: clusterRouter, + dashboardRouter: dashboardRouter, + helmAppRouter: helmAppRouter, + environmentRouter: environmentRouter, + k8sApplicationRouter: k8sApplicationRouter, + chartRepositoryRouter: chartRepositoryRouter, + appStoreDiscoverRouter: appStoreDiscoverRouter, + appStoreValuesRouter: appStoreValuesRouter, + appStoreDeploymentRouter: appStoreDeploymentRouter, + chartProviderRouter: chartProviderRouter, + dockerRegRouter: dockerRegRouter, + dashboardTelemetryRouter: dashboardTelemetryRouter, + commonDeploymentRouter: commonDeploymentRouter, + externalLinksRouter: externalLinkRouter, + moduleRouter: moduleRouter, + serverRouter: serverRouter, + apiTokenRouter: apiTokenRouter, + k8sCapacityRouter: k8sCapacityRouter, + webhookHelmRouter: webhookHelmRouter, + userAttributesRouter: userAttributesRouter, + telemetryRouter: telemetryRouter, + userTerminalAccessRouter: userTerminalAccessRouter, + attributesRouter: attributesRouter, + appRouter: appRouter, + rbacRoleRouter: rbacRoleRouter, + argoApplicationRouter: argoApplicationRouter, + fluxApplicationRouter: fluxApplicationRouter, + userResourceRouter: userResourceRouter, + infraOverviewRouter: infraOverviewRouter, + globalAuthorisationConfigRouter: globalAuthorisationConfigRouter, } return r } @@ -308,4 +312,7 @@ func (r *MuxRouter) Init() { infraOverviewRouter := r.Router.PathPrefix("/orchestrator/overview/infra").Subrouter() r.infraOverviewRouter.InitInfraOverviewRouter(infraOverviewRouter) + authorisationConfigRouter := r.Router.PathPrefix("/orchestrator/authorisation").Subrouter() + r.globalAuthorisationConfigRouter.InitAuthorisationConfigRouter(authorisationConfigRouter) + } diff --git a/cmd/external-app/wire.go b/cmd/external-app/wire.go index 18331bc4df..0206c3b493 100644 --- a/cmd/external-app/wire.go +++ b/cmd/external-app/wire.go @@ -31,6 +31,7 @@ import ( appStoreDiscover "github.com/devtron-labs/devtron/api/appStore/discover" appStoreValues "github.com/devtron-labs/devtron/api/appStore/values" "github.com/devtron-labs/devtron/api/argoApplication" + globalConfigAPI "github.com/devtron-labs/devtron/api/auth/authorisation/globalConfig" "github.com/devtron-labs/devtron/api/auth/sso" "github.com/devtron-labs/devtron/api/auth/user" chartRepo "github.com/devtron-labs/devtron/api/chartRepo" @@ -108,6 +109,7 @@ func InitializeApp() (*App, error) { user.UserWireSet, sso.SsoConfigWireSet, AuthWireSet, + globalConfigAPI.GlobalConfigWireSet, util4.GetRuntimeConfig, util4.NewK8sUtil, externalLink.ExternalLinkWireSet, From 96ccd33a57304a1e3299c634b7247c45a56fc19c Mon Sep 17 00:00:00 2001 From: systemsdt <129372406+systemsdt@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:26:56 +0530 Subject: [PATCH 04/19] release: PR for v2.1.0 (#6928) * Updated release-notes files * Updated release notes * Updated release notes * Updated release notes * Updated release notes * Updated release notes * Updated release notes * Updated release notes * Updated release notes * Updated devtron to 59238e84-434-38692 tag in values file * Updated kubelink to 6b408df4-564-38694 tag in values file * Updated dashboard to d87d9a07-690-38693 tag in values file * Updated release notes * Updated release notes * Updated release notes * Updated release notes * Updated kubewatch to fbde4d5e-419-38744 tag in values file * Updated hyperion to 37b07f15-280-38743 tag in values file * Updated devtron to 37b07f15-434-38746 tag in values file * Updated kubelink to fbde4d5e-564-38749 tag in values file * Updated git-sensor to fbde4d5e-200-38750 tag in values file * Updated lens to fbde4d5e-333-38752 tag in values file * Updated dashboard to d4a16ea7-690-38751 tag in values file * Updated ci-runner to fbde4d5e-138-38754 tag in values file * Updated image-scanner to fbde4d5e-141-38756 tag in values file * Updated notifier to 580d409b-372-38755 tag in values file * Updated chart-sync to fbde4d5e-836-38757 tag in values file * Updated the version in scripts * Update TimescaleDB password secret reference * Bump version from 0.22.99 to 0.23.1 * Add CLUSTER_OVERVIEW_MAX_STALE_DATA_SECONDS variable * Update releasenotes.md * Update release-notes-v2.1.0.md * Updated devtron to 634eb598-434-38762 tag in values file * Updated hyperion to 634eb598-280-38763 tag in values file --------- Co-authored-by: akshatsinha007 <156403098+akshatsinha007@users.noreply.github.com> --- CHANGELOG/release-notes-v2.1.0.md | 14 ++++ charts/devtron/Chart.yaml | 4 +- charts/devtron/devtron-bom.yaml | 24 +++--- .../devtron/templates/configmap-secret.yaml | 2 +- charts/devtron/templates/devtron.yaml | 1 + charts/devtron/values.yaml | 24 +++--- devtron-images.txt.source | 22 ++--- manifests/install/devtron-installer.yaml | 2 +- manifests/installation-script | 2 +- releasenotes.md | 83 ++----------------- 10 files changed, 64 insertions(+), 114 deletions(-) create mode 100644 CHANGELOG/release-notes-v2.1.0.md diff --git a/CHANGELOG/release-notes-v2.1.0.md b/CHANGELOG/release-notes-v2.1.0.md new file mode 100644 index 0000000000..bea14186e0 --- /dev/null +++ b/CHANGELOG/release-notes-v2.1.0.md @@ -0,0 +1,14 @@ +## v2.1.0 + +## Enhancements +- feat: auto assign permission group ( https://github.com/devtron-labs/devtron/issues/6911 ) +## Bugs +- fix: prevent exposure of internal-only attributes in API responses and requests (#6917) +- fix: append filtered cluster details to the cluster detail list in capacity handler (#6915) +- fix: enhance cluster overview response with raw cluster capacity details and caching support (#6914) +- fix: Handle cluster capacity fetch errors by returning detailed connection failure status (#6912) +## Others +- sync: main (#6920) +- chore: Adds scarf pixel (#6918) +- misc: add clientIP in audit log (#6908) +- misc: Refactor vulnerability query implementation and cleanup unused code (#6907) diff --git a/charts/devtron/Chart.yaml b/charts/devtron/Chart.yaml index 585c4b96d0..8e0476d66d 100644 --- a/charts/devtron/Chart.yaml +++ b/charts/devtron/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: devtron-operator -appVersion: 2.0.0 +appVersion: 2.1.0 description: Chart to configure and install Devtron. Devtron is a Kubernetes Orchestration system. keywords: - Devtron @@ -11,7 +11,7 @@ keywords: - argocd - Hyperion engine: gotpl -version: 0.22.99 +version: 0.23.1 sources: - https://github.com/devtron-labs/charts dependencies: diff --git a/charts/devtron/devtron-bom.yaml b/charts/devtron/devtron-bom.yaml index fe719fad27..de89b6d700 100644 --- a/charts/devtron/devtron-bom.yaml +++ b/charts/devtron/devtron-bom.yaml @@ -15,7 +15,7 @@ global: PG_DATABASE: orchestrator extraManifests: [] installer: - release: "v2.0.0" + release: "v2.1.0" registry: "" image: "inception" tag: "473deaa4-185-21582" @@ -41,13 +41,13 @@ components: FEATURE_CODE_MIRROR_ENABLE: "true" FEATURE_GROUPED_APP_LIST_FILTERS_ENABLE: "true" registry: "" - image: "dashboard:b48d0910-690-38228" + image: "dashboard:d4a16ea7-690-38751" imagePullPolicy: IfNotPresent healthPort: 8080 devtron: registry: "" - image: "hyperion:f0c18f20-280-38148" - cicdImage: "devtron:f0c18f20-434-38146" + image: "hyperion:634eb598-280-38763" + cicdImage: "devtron:634eb598-434-38762" imagePullPolicy: IfNotPresent customOverrides: {} podSecurityContext: @@ -61,7 +61,7 @@ components: healthPort: 8080 ciRunner: registry: "" - image: "ci-runner:6b408df4-138-38163" + image: "ci-runner:fbde4d5e-138-38754" argocdDexServer: registry: "" image: "dex:v2.30.2" @@ -70,7 +70,7 @@ components: authenticator: "authenticator:e414faff-393-13273" kubelink: registry: "" - image: "kubelink:6b408df4-564-38159" + image: "kubelink:fbde4d5e-564-38749" imagePullPolicy: IfNotPresent configs: ENABLE_HELM_RELEASE_CACHE: "true" @@ -93,7 +93,7 @@ components: healthPort: 50052 kubewatch: registry: "" - image: "kubewatch:6b408df4-419-38172" + image: "kubewatch:fbde4d5e-419-38744" imagePullPolicy: IfNotPresent healthPort: 8080 configs: @@ -118,7 +118,7 @@ components: image: postgres_exporter:v0.10.1 gitsensor: registry: "" - image: "git-sensor:6b408df4-200-38174" + image: "git-sensor:fbde4d5e-200-38750" imagePullPolicy: IfNotPresent serviceMonitor: enabled: false @@ -136,7 +136,7 @@ components: # Values for lens lens: registry: "" - image: "lens:6b408df4-333-38167" + image: "lens:fbde4d5e-333-38752" imagePullPolicy: IfNotPresent configs: GIT_SENSOR_PROTOCOL: GRPC @@ -171,7 +171,7 @@ components: entMigratorImage: "devtron-utils:geni-v1.1.4" chartSync: registry: "" - image: chart-sync:6b408df4-836-38155 + image: chart-sync:fbde4d5e-836-38757 schedule: "0 19 * * *" podSecurityContext: fsGroup: 1001 @@ -209,7 +209,7 @@ workflowController: IMDSv1ExecutorImage: "argoexec:v3.0.7" security: imageScanner: - image: "image-scanner:6b408df4-141-38158" + image: "image-scanner:fbde4d5e-141-38756" healthPort: 8080 configs: TRIVY_DB_REPOSITORY: mirror.gcr.io/aquasec/trivy-db @@ -220,7 +220,7 @@ security: tag: 4.3.6 # Values for notifier integration notifier: - image: "notifier:5c4b5b3a-372-38153" + image: "notifier:580d409b-372-38755" healthPort: 3000 minio: image: "minio:RELEASE.2021-02-14T04-01-33Z" diff --git a/charts/devtron/templates/configmap-secret.yaml b/charts/devtron/templates/configmap-secret.yaml index 33692460c6..f9c48511d5 100644 --- a/charts/devtron/templates/configmap-secret.yaml +++ b/charts/devtron/templates/configmap-secret.yaml @@ -8,7 +8,7 @@ {{- $DEX_CSTOREKEY := include "getOrGeneratePass" (dict "Namespace" "devtroncd" "Kind" "Secret" "Name" "devtron-secret" "Key" "DEX_CSTOREKEY") }} {{- $postgresPwd := include "getOrGeneratePass" (dict "Namespace" "devtroncd" "Kind" "Secret" "Name" "postgresql-postgresql" "Key" "postgresql-password") }} {{- $WEBHOOK_TOKEN := include "getOrGeneratePass" (dict "Namespace" "devtroncd" "Kind" "Secret" "Name" "devtron-secret" "Key" "WEBHOOK_TOKEN") }} -{{- $TIMESCALE_PASSWORD := include "getOrGeneratePass" (dict "Namespace" "devtroncd" "Kind" "Secret" "Name" "timescaledb-cluster-pg15-superuser" "Key" "password") }} +{{- $TIMESCALE_PASSWORD := include "getOrGeneratePass" (dict "Namespace" "devtroncd" "Kind" "Secret" "Name" "timescaledb-secret" "Key" "POSTGRES_PASSWORD") }} {{- if $.Values.installer.modules }} {{- if has "cicd" $.Values.installer.modules }} diff --git a/charts/devtron/templates/devtron.yaml b/charts/devtron/templates/devtron.yaml index d4664939ba..d1392fd129 100644 --- a/charts/devtron/templates/devtron.yaml +++ b/charts/devtron/templates/devtron.yaml @@ -10,6 +10,7 @@ metadata: annotations: "helm.sh/resource-policy": keep data: + CLUSTER_OVERVIEW_MAX_STALE_DATA_SECONDS: "330" DEVTRON_HELM_RELEASE_NAME: {{ $.Release.Name }} DEVTRON_HELM_RELEASE_NAMESPACE: {{ $.Release.Namespace }} FEATURE_MIGRATE_ARGOCD_APPLICATION_ENABLE: "true" diff --git a/charts/devtron/values.yaml b/charts/devtron/values.yaml index 1aabe83375..2b32cd5fbc 100644 --- a/charts/devtron/values.yaml +++ b/charts/devtron/values.yaml @@ -42,7 +42,7 @@ nfs: extraManifests: [] installer: repo: "devtron-labs/devtron" - release: "v2.0.0" + release: "v2.1.0" registry: "" image: inception tag: 473deaa4-185-21582 @@ -97,13 +97,13 @@ components: FEATURE_CODE_MIRROR_ENABLE: "true" FEATURE_GROUPED_APP_LIST_FILTERS_ENABLE: "true" registry: "" - image: "dashboard:b48d0910-690-38228" + image: "dashboard:d4a16ea7-690-38751" imagePullPolicy: IfNotPresent healthPort: 8080 devtron: registry: "" - image: "hyperion:f0c18f20-280-38148" - cicdImage: "devtron:f0c18f20-434-38146" + image: "hyperion:634eb598-280-38763" + cicdImage: "devtron:634eb598-434-38762" imagePullPolicy: IfNotPresent customOverrides: {} healthPort: 8080 @@ -140,7 +140,7 @@ components: # - devtron.example.com ciRunner: registry: "" - image: "ci-runner:6b408df4-138-38163" + image: "ci-runner:fbde4d5e-138-38754" # Add annotations for ci-runner & cd-runner serviceAccount. annotations: {} argocdDexServer: @@ -151,7 +151,7 @@ components: authenticator: "authenticator:e414faff-393-13273" kubelink: registry: "" - image: "kubelink:6b408df4-564-38159" + image: "kubelink:fbde4d5e-564-38749" imagePullPolicy: IfNotPresent healthPort: 50052 podSecurityContext: @@ -174,7 +174,7 @@ components: keyName: postgresql-password kubewatch: registry: "" - image: "kubewatch:6b408df4-419-38172" + image: "kubewatch:fbde4d5e-419-38744" imagePullPolicy: IfNotPresent healthPort: 8080 configs: @@ -201,7 +201,7 @@ components: volumeSize: "20Gi" gitsensor: registry: "" - image: "git-sensor:6b408df4-200-38174" + image: "git-sensor:fbde4d5e-200-38750" imagePullPolicy: IfNotPresent serviceMonitor: enabled: false @@ -219,7 +219,7 @@ components: # Values for lens lens: registry: "" - image: "lens:6b408df4-333-38167" + image: "lens:fbde4d5e-333-38752" imagePullPolicy: IfNotPresent secrets: {} resources: {} @@ -256,7 +256,7 @@ components: entMigratorImage: "devtron-utils:geni-v1.1.4" chartSync: registry: "" - image: chart-sync:6b408df4-836-38155 + image: chart-sync:fbde4d5e-836-38757 schedule: "0 19 * * *" extraConfigs: {} podSecurityContext: @@ -412,7 +412,7 @@ argo-cd: security: enabled: false imageScanner: - image: "image-scanner:6b408df4-141-38158" + image: "image-scanner:fbde4d5e-141-38756" healthPort: 8080 configs: TRIVY_DB_REPOSITORY: mirror.gcr.io/aquasec/trivy-db @@ -431,7 +431,7 @@ security: notifier: enabled: false imagePullPolicy: IfNotPresent - image: "notifier:5c4b5b3a-372-38153" + image: "notifier:580d409b-372-38755" configs: CD_ENVIRONMENT: PROD secrets: {} diff --git a/devtron-images.txt.source b/devtron-images.txt.source index 568b10a494..b3849be494 100644 --- a/devtron-images.txt.source +++ b/devtron-images.txt.source @@ -7,26 +7,26 @@ quay.io/devtron/authenticator:e414faff-393-13273 quay.io/devtron/bats:v1.4.1 quay.io/devtron/busybox:1.31.1 quay.io/devtron/centos-k8s-utils:latest -quay.io/devtron/chart-sync:6b408df4-836-38155 -quay.io/devtron/ci-runner:6b408df4-138-38163 +quay.io/devtron/chart-sync:fbde4d5e-836-38757 +quay.io/devtron/ci-runner:fbde4d5e-138-38754 quay.io/devtron/clair:4.3.6 quay.io/devtron/curl:7.73.0 -quay.io/devtron/dashboard:b48d0910-690-38228 +quay.io/devtron/dashboard:d4a16ea7-690-38751 quay.io/devtron/devtron-utils:dup-chart-repo-v1.1.0 -quay.io/devtron/devtron:f0c18f20-434-38146 +quay.io/devtron/devtron:634eb598-434-38762 quay.io/devtron/dex:v2.30.2 -quay.io/devtron/git-sensor:6b408df4-200-38174 +quay.io/devtron/git-sensor:fbde4d5e-200-38750 quay.io/devtron/grafana:7.3.1 -quay.io/devtron/hyperion:f0c18f20-280-38148 -quay.io/devtron/image-scanner:6b408df4-141-38158 +quay.io/devtron/hyperion:634eb598-280-38763 +quay.io/devtron/image-scanner:fbde4d5e-141-38756 quay.io/devtron/inception:473deaa4-185-21582 quay.io/devtron/k8s-sidecar:1.1.0 quay.io/devtron/k8s-utils:tutum-curl quay.io/devtron/k9s-k8s-utils:latest quay.io/devtron/kubectl:latest -quay.io/devtron/kubelink:6b408df4-564-38159 -quay.io/devtron/kubewatch:6b408df4-419-38172 -quay.io/devtron/lens:6b408df4-333-38167 +quay.io/devtron/kubelink:fbde4d5e-564-38749 +quay.io/devtron/kubewatch:fbde4d5e-419-38744 +quay.io/devtron/lens:fbde4d5e-333-38752 quay.io/devtron/migrator:v4.16.2 quay.io/devtron/minideb:latest quay.io/devtron/minio-mc:RELEASE.2021-02-14T04-28-06Z @@ -34,7 +34,7 @@ quay.io/devtron/minio:RELEASE.2021-02-14T04-01-33Z quay.io/devtron/nats-box quay.io/devtron/nats-server-config-reloader:0.6.2 quay.io/devtron/nats:2.9.3-alpine -quay.io/devtron/notifier:5c4b5b3a-372-38153 +quay.io/devtron/notifier:580d409b-372-38755 quay.io/devtron/postgres:14.9 quay.io/devtron/postgres_exporter:v0.10.1 quay.io/devtron/postgres_exporter:v0.4.7 diff --git a/manifests/install/devtron-installer.yaml b/manifests/install/devtron-installer.yaml index 837d98b963..5e168f8482 100644 --- a/manifests/install/devtron-installer.yaml +++ b/manifests/install/devtron-installer.yaml @@ -4,4 +4,4 @@ metadata: name: installer-devtron namespace: devtroncd spec: - url: https://raw.githubusercontent.com/devtron-labs/devtron/v2.0.0/manifests/installation-script + url: https://raw.githubusercontent.com/devtron-labs/devtron/v2.1.0/manifests/installation-script diff --git a/manifests/installation-script b/manifests/installation-script index daaafa5557..d68f24dfbd 100644 --- a/manifests/installation-script +++ b/manifests/installation-script @@ -1,4 +1,4 @@ -LTAG="v2.0.0"; +LTAG="v2.1.0"; REPO_RAW_URL="https://raw.githubusercontent.com/devtron-labs/devtron/"; shebang = `#!/bin/bash `; diff --git a/releasenotes.md b/releasenotes.md index 3b44e36bc5..ab791210c5 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -1,78 +1,13 @@ -## v2.0.0 -Devtron 2.0 focuses on improving Kubernetes operability at scale. It introduces centralized platform visibility, a restructured UI for faster navigation, and foundational security improvements—reducing operational overhead across CI/CD, infrastructure, and access management. - ---- +## v2.1.0 ## Enhancements - -### Overview Dashboards -We've introduced centralized overview dashboards to give teams instant visibility across critical dimensions of the system. - -- **Applications Overview** - View application health metrics and CI/CD pipeline activity in one place to quickly assess delivery performance and identify issues early. - -- **Infrastructure Overview** - Gain clear insights into cluster utilization and resource allocation for better visibility into Kubernetes capacity, usage, and optimization opportunities. - -- **Security Overview** - Get an aggregated view of your security posture to identify risks, track vulnerabilities, and monitor overall security status effectively. - ---- - -### Reimagined Platform UI -Devtron's UI has been restructured into logical modules, making the platform more intuitive, discoverable, and easier to navigate. - -- **Application Management** - Deploy and manage applications, access application groups, and apply bulk edits from a unified workspace. - -- **Infrastructure Management** - Manage applications deployed via Helm, Argo CD, and Flux CD, access the chart store, and explore your Kubernetes resources using the resource browser. - -- **Security Centre** - Review vulnerability reports, manage security scans, and enforce security policies from a single control plane. - -- **Automation Enablement** - Configure and schedule job orchestration to power automated workflows and reduce manual operational overhead. - -- **Global Configuration** - Configure SSO, manage clusters and environments, register container registries, and define authorization and access policies centrally. - ---- - -### Command Bar for Faster Navigation -Use the Command Bar (Ctrl + K on Windows/Linux, Cmd + K on macOS) to quickly jump to any screen across the platform and revisit recently accessed items—reducing clicks and speeding up navigation between workflows. - ---- - -### Additional Enhancements -- feat: Rollout 5.2.0 (#6889) -- feat: Added support for tcp in virtual service and changed the apiVersion for externalSecrets (#6892) -- feat: add helm_take_ownership and helm_redeployment_request columns to user_deployment_request table (#6888) -- feat: Added support to override container name (#6880) -- feat: Increase max length for TeamRequest name field (#6876) -- feat: Added namespace support for virtualService and destinationRule (#6868) -- feat: feature flag for encryption (#6856) -- feat: encryption for db credentials (#6852) - ---- - +- feat: auto assign permission group ( https://github.com/devtron-labs/devtron/issues/6911 ) ## Bugs -- fix: migrate proxy chart dependencies and refactor related functions (#6899) -- fix: enhance validation and error handling in cluster update process (#6887) -- fix: Invalid type casting error for custom charts (#6883) -- fix: validation on team name (#6872) -- fix: sql injection (#6861) -- fix: user manager fix (#6854) - ---- - +- fix: prevent exposure of internal-only attributes in API responses and requests (#6917) +- fix: append filtered cluster details to the cluster detail list in capacity handler (#6915) +- fix: enhance cluster overview response with raw cluster capacity details and caching support (#6914) +- fix: Handle cluster capacity fetch errors by returning detailed connection failure status (#6912) ## Others -- misc: Add support for migrating plugin metadata to parent metadata (#6902) -- misc: update UserDeploymentRequestWithAdditionalFields struct to include tableName for PostgreSQL compatibility (#6896) -- chore: rename SQL migration files for consistency (#6885) -- misc: Vc empty ns fix (#6871) -- misc: added validation on create environment (#6859) -- misc: migration unique constraint on mpc (#6851) -- misc: helm app details API spec (#6850) -- misc: api Spec Added for draft (#6849) -- misc: api Specs added for lock config (#6847) +- chore: Adds scarf pixel (#6918) +- misc: add clientIP in audit log (#6908) +- misc: Refactor vulnerability query implementation and cleanup unused code (#6907) From 001eaae89fd261aa3c8cc429c3a739c1c201d4ae Mon Sep 17 00:00:00 2001 From: Neha Sharma Date: Wed, 11 Mar 2026 17:13:31 +0530 Subject: [PATCH 05/19] add support of container name in cronjob --- .../cronjob-chart_1-6-0/templates/_pod_template_spec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/devtron-reference-helm-charts/cronjob-chart_1-6-0/templates/_pod_template_spec.yaml b/scripts/devtron-reference-helm-charts/cronjob-chart_1-6-0/templates/_pod_template_spec.yaml index 8188d5e970..b3bb32f9a4 100644 --- a/scripts/devtron-reference-helm-charts/cronjob-chart_1-6-0/templates/_pod_template_spec.yaml +++ b/scripts/devtron-reference-helm-charts/cronjob-chart_1-6-0/templates/_pod_template_spec.yaml @@ -144,7 +144,7 @@ containers: {{- if $.Values.containers }} {{ toYaml $.Values.containers | indent 2 -}} {{- end }} - - name: {{ $.Chart.Name }} + - name: "{{ if $.Values.containerName }}{{ $.Values.containerName }}{{ else }}{{ $.Chart.Name }}{{ end }}" image: "{{ .Values.server.deployment.image }}:{{ .Values.server.deployment.image_tag }}" imagePullPolicy: {{ $.Values.image.pullPolicy }} {{- if $.Values.containerExtraSpecs }} From e1cfcd63863244cec8103547318ca4cf2dbdf1dc Mon Sep 17 00:00:00 2001 From: satya_prakash <155617493+SATYAsasini@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:25:04 +0530 Subject: [PATCH 06/19] fix: clusterId check for modifying triggers for cluster level notification (#6932) --- internal/sql/repository/NotificationSettingsRepository.go | 4 ++-- pkg/notifier/NotificationConfigService.go | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/sql/repository/NotificationSettingsRepository.go b/internal/sql/repository/NotificationSettingsRepository.go index 91934c72f3..2f5f0b2386 100644 --- a/internal/sql/repository/NotificationSettingsRepository.go +++ b/internal/sql/repository/NotificationSettingsRepository.go @@ -370,8 +370,8 @@ type SearchRequest struct { func (impl *NotificationSettingsRepositoryImpl) FetchNotificationSettingGroupBy(viewId int) ([]NotificationSettings, error) { var ns []NotificationSettings - queryTemp := "select ns.team_id,ns.env_id,ns.app_id,ns.pipeline_id,ns.pipeline_type from notification_settings ns" + - " where ns.view_id=? group by 1,2,3,4,5;" + queryTemp := "select ns.team_id,ns.env_id,ns.app_id,ns.pipeline_id,ns.pipeline_type,ns.cluster_id from notification_settings ns" + + " where ns.view_id=? group by 1,2,3,4,5,6;" _, err := impl.dbConnection.Query(&ns, queryTemp, viewId) if err != nil { return nil, err diff --git a/pkg/notifier/NotificationConfigService.go b/pkg/notifier/NotificationConfigService.go index aa6cad0651..d19da3138e 100644 --- a/pkg/notifier/NotificationConfigService.go +++ b/pkg/notifier/NotificationConfigService.go @@ -589,6 +589,7 @@ func (impl *NotificationConfigServiceImpl) updateNotificationSetting(notificatio notificationSettingsRequest.PipelineId = nsConfig.PipelineId notificationSettingsRequest.PipelineType = nsConfig.PipelineType notificationSettingsRequest.Providers = nsConfig.Providers + notificationSettingsRequest.ClusterId = nsConfig.ClusterId var notificationSettings []repository.NotificationSettings nsOptions, err := impl.notificationSettingsRepository.FetchNotificationSettingGroupBy(notificationSettingsRequest.Id) if err != nil { From 1188d0b95abba9800e14c2acda9cd38c57318540 Mon Sep 17 00:00:00 2001 From: satya_prakash <155617493+SATYAsasini@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:54:40 +0530 Subject: [PATCH 07/19] fix: auto assign permission group related fixes (#6934) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: sync auto-assigned groups with casbin_rule user→group policies * fix: support token for rbac check for clusters * fix: support token for checkUser roles * fix: support token based authentication for policy rest handlers * fix: check for user isGroupClaims active in all rbac related functions * fix: add email in case of devtron system managed * fix: ea mode dependency updates --- api/auth/user/UserRestHandler.go | 3 +- api/cluster/ClusterRestHandler.go | 6 +- .../application/k8sApplicationRestHandler.go | 2 +- api/restHandler/PolicyRestHandler.go | 4 +- cmd/external-app/wire_gen.go | 108 +++++++++-------- pkg/auth/authorisation/casbin/rbac.go | 5 +- pkg/auth/user/UserService.go | 6 +- pkg/auth/user/UserService_ent.go | 53 ++++++--- pkg/auth/user/mocks/UserService.go | 8 +- pkg/cluster/ClusterService.go | 109 +++++++++++------- pkg/cluster/rbac/ClusterRbacService.go | 6 +- pkg/k8s/application/k8sApplicationService.go | 6 +- .../mocks/K8sApplicationService.go | 14 +-- pkg/userResource/UserResourceService.go | 6 +- wire_gen.go | 2 +- 15 files changed, 203 insertions(+), 135 deletions(-) diff --git a/api/auth/user/UserRestHandler.go b/api/auth/user/UserRestHandler.go index de8093a1da..37188c14bd 100644 --- a/api/auth/user/UserRestHandler.go +++ b/api/auth/user/UserRestHandler.go @@ -749,7 +749,8 @@ func (handler UserRestHandlerImpl) CheckUserRoles(w http.ResponseWriter, r *http common.HandleUnauthorized(w, r) return } - roles, err := handler.userService.CheckUserRoles(userId, "") + token := r.Header.Get("token") + roles, err := handler.userService.CheckUserRoles(userId, token) if err != nil { handler.logger.Errorw("service err, CheckUserRoles", "err", err, "userId", userId) common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) diff --git a/api/cluster/ClusterRestHandler.go b/api/cluster/ClusterRestHandler.go index f092a6de84..de95bc1e9c 100644 --- a/api/cluster/ClusterRestHandler.go +++ b/api/cluster/ClusterRestHandler.go @@ -686,7 +686,7 @@ func (impl ClusterRestHandlerImpl) HandleRbacForClusterNamespace(userId int32, t if ok := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); ok { return clusterNamespaces, nil } - roles, err := impl.clusterService.FetchRolesFromGroup(userId) + roles, err := impl.clusterService.FetchRolesFromGroup(userId, token) if err != nil { impl.logger.Errorw("error on fetching user roles for cluster list", "err", err) return nil, err @@ -740,7 +740,7 @@ func (impl ClusterRestHandlerImpl) GetClusterNamespaces(w http.ResponseWriter, r return } - allClusterNamespaces, err := impl.clusterService.FindAllNamespacesByUserIdAndClusterId(userId, clusterId, isActionUserSuperAdmin) + allClusterNamespaces, err := impl.clusterService.FindAllNamespacesByUserIdAndClusterId(userId, clusterId, isActionUserSuperAdmin, token) if err != nil { // Check if it's a cluster connectivity error and return appropriate status code if err.Error() == cluster.ErrClusterNotReachable { @@ -767,7 +767,7 @@ func (impl ClusterRestHandlerImpl) FindAllForClusterPermission(w http.ResponseWr if ok := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); ok { isActionUserSuperAdmin = true } - clusterList, err := impl.clusterService.FindAllForClusterByUserId(userId, isActionUserSuperAdmin) + clusterList, err := impl.clusterService.FindAllForClusterByUserId(userId, isActionUserSuperAdmin, token) if err != nil { impl.logger.Errorw("error in deleting cluster", "err", err) common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) diff --git a/api/k8s/application/k8sApplicationRestHandler.go b/api/k8s/application/k8sApplicationRestHandler.go index 517bef5991..60a8f39cfd 100644 --- a/api/k8s/application/k8sApplicationRestHandler.go +++ b/api/k8s/application/k8sApplicationRestHandler.go @@ -903,7 +903,7 @@ func (handler *K8sApplicationRestHandlerImpl) GetAllApiResources(w http.Response } // get data from service - response, err := handler.k8sApplicationService.GetAllApiResources(r.Context(), clusterId, isSuperAdmin, userId) + response, err := handler.k8sApplicationService.GetAllApiResources(r.Context(), clusterId, isSuperAdmin, userId, token) if err != nil { handler.logger.Errorw("error in getting api-resources", "clusterId", clusterId, "err", err) common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) diff --git a/api/restHandler/PolicyRestHandler.go b/api/restHandler/PolicyRestHandler.go index c10ca49e68..8c0538171f 100644 --- a/api/restHandler/PolicyRestHandler.go +++ b/api/restHandler/PolicyRestHandler.go @@ -104,7 +104,7 @@ func (impl PolicyRestHandlerImpl) SavePolicy(w http.ResponseWriter, r *http.Requ } } else { // for global and cluster level check super admin access only - roles, err := impl.userService.CheckUserRoles(userId, "") + roles, err := impl.userService.CheckUserRoles(userId, token) if err != nil { common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) return @@ -174,7 +174,7 @@ func (impl PolicyRestHandlerImpl) UpdatePolicy(w http.ResponseWriter, r *http.Re } } else { // for global and cluster level check super admin access only - roles, err := impl.userService.CheckUserRoles(userId, "") + roles, err := impl.userService.CheckUserRoles(userId, token) if err != nil { common.WriteJsonResp(w, err, "Failed to get user by id", http.StatusInternalServerError) return diff --git a/cmd/external-app/wire_gen.go b/cmd/external-app/wire_gen.go index d36dd1fa90..a48fd1beca 100644 --- a/cmd/external-app/wire_gen.go +++ b/cmd/external-app/wire_gen.go @@ -20,6 +20,7 @@ import ( "github.com/devtron-labs/devtron/api/appStore/discover" "github.com/devtron-labs/devtron/api/appStore/values" argoApplication2 "github.com/devtron-labs/devtron/api/argoApplication" + globalConfig2 "github.com/devtron-labs/devtron/api/auth/authorisation/globalConfig" sso2 "github.com/devtron-labs/devtron/api/auth/sso" user2 "github.com/devtron-labs/devtron/api/auth/user" chartRepo2 "github.com/devtron-labs/devtron/api/chartRepo" @@ -54,23 +55,23 @@ import ( "github.com/devtron-labs/devtron/client/dashboard" "github.com/devtron-labs/devtron/client/grafana" telemetry2 "github.com/devtron-labs/devtron/client/telemetry" - repository5 "github.com/devtron-labs/devtron/internal/sql/repository" + repository6 "github.com/devtron-labs/devtron/internal/sql/repository" "github.com/devtron-labs/devtron/internal/sql/repository/app" "github.com/devtron-labs/devtron/internal/sql/repository/appStatus" "github.com/devtron-labs/devtron/internal/sql/repository/chartConfig" "github.com/devtron-labs/devtron/internal/sql/repository/deploymentConfig" - repository7 "github.com/devtron-labs/devtron/internal/sql/repository/dockerRegistry" + repository8 "github.com/devtron-labs/devtron/internal/sql/repository/dockerRegistry" "github.com/devtron-labs/devtron/internal/sql/repository/pipelineConfig" "github.com/devtron-labs/devtron/internal/util" "github.com/devtron-labs/devtron/pkg/apiToken" app2 "github.com/devtron-labs/devtron/pkg/app" "github.com/devtron-labs/devtron/pkg/app/dbMigration" - repository9 "github.com/devtron-labs/devtron/pkg/appStore/chartGroup/repository" + repository10 "github.com/devtron-labs/devtron/pkg/appStore/chartGroup/repository" "github.com/devtron-labs/devtron/pkg/appStore/chartProvider" "github.com/devtron-labs/devtron/pkg/appStore/discover/repository" service3 "github.com/devtron-labs/devtron/pkg/appStore/discover/service" read3 "github.com/devtron-labs/devtron/pkg/appStore/installedApp/read" - repository6 "github.com/devtron-labs/devtron/pkg/appStore/installedApp/repository" + repository7 "github.com/devtron-labs/devtron/pkg/appStore/installedApp/repository" service2 "github.com/devtron-labs/devtron/pkg/appStore/installedApp/service" "github.com/devtron-labs/devtron/pkg/appStore/installedApp/service/EAMode" "github.com/devtron-labs/devtron/pkg/appStore/installedApp/service/EAMode/deployment" @@ -84,20 +85,22 @@ import ( "github.com/devtron-labs/devtron/pkg/attributes" "github.com/devtron-labs/devtron/pkg/auth/authentication" "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin" + "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig" + "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig/repository" "github.com/devtron-labs/devtron/pkg/auth/sso" "github.com/devtron-labs/devtron/pkg/auth/user" - "github.com/devtron-labs/devtron/pkg/auth/user/repository" + repository2 "github.com/devtron-labs/devtron/pkg/auth/user/repository" read10 "github.com/devtron-labs/devtron/pkg/build/git/gitMaterial/read" - repository12 "github.com/devtron-labs/devtron/pkg/build/git/gitMaterial/repository" + repository13 "github.com/devtron-labs/devtron/pkg/build/git/gitMaterial/repository" "github.com/devtron-labs/devtron/pkg/chartRepo" "github.com/devtron-labs/devtron/pkg/chartRepo/repository" "github.com/devtron-labs/devtron/pkg/cluster" "github.com/devtron-labs/devtron/pkg/cluster/environment" read8 "github.com/devtron-labs/devtron/pkg/cluster/environment/read" - repository4 "github.com/devtron-labs/devtron/pkg/cluster/environment/repository" + repository5 "github.com/devtron-labs/devtron/pkg/cluster/environment/repository" rbac2 "github.com/devtron-labs/devtron/pkg/cluster/rbac" read2 "github.com/devtron-labs/devtron/pkg/cluster/read" - repository3 "github.com/devtron-labs/devtron/pkg/cluster/repository" + repository4 "github.com/devtron-labs/devtron/pkg/cluster/repository" "github.com/devtron-labs/devtron/pkg/clusterTerminalAccess" "github.com/devtron-labs/devtron/pkg/commonService" delete2 "github.com/devtron-labs/devtron/pkg/delete" @@ -109,13 +112,13 @@ import ( "github.com/devtron-labs/devtron/pkg/externalLink" "github.com/devtron-labs/devtron/pkg/fluxApplication" "github.com/devtron-labs/devtron/pkg/genericNotes" - repository8 "github.com/devtron-labs/devtron/pkg/genericNotes/repository" + repository9 "github.com/devtron-labs/devtron/pkg/genericNotes/repository" k8s2 "github.com/devtron-labs/devtron/pkg/k8s" "github.com/devtron-labs/devtron/pkg/k8s/application" "github.com/devtron-labs/devtron/pkg/k8s/capacity" "github.com/devtron-labs/devtron/pkg/k8s/informer" "github.com/devtron-labs/devtron/pkg/kubernetesResourceAuditLogs" - repository10 "github.com/devtron-labs/devtron/pkg/kubernetesResourceAuditLogs/repository" + repository11 "github.com/devtron-labs/devtron/pkg/kubernetesResourceAuditLogs/repository" "github.com/devtron-labs/devtron/pkg/module" bean2 "github.com/devtron-labs/devtron/pkg/module/bean" read7 "github.com/devtron-labs/devtron/pkg/module/read" @@ -126,14 +129,14 @@ import ( config4 "github.com/devtron-labs/devtron/pkg/overview/config" "github.com/devtron-labs/devtron/pkg/pipeline" "github.com/devtron-labs/devtron/pkg/policyGovernance/security/scanTool" - repository11 "github.com/devtron-labs/devtron/pkg/policyGovernance/security/scanTool/repository" + repository12 "github.com/devtron-labs/devtron/pkg/policyGovernance/security/scanTool/repository" "github.com/devtron-labs/devtron/pkg/server" "github.com/devtron-labs/devtron/pkg/server/config" "github.com/devtron-labs/devtron/pkg/server/store" "github.com/devtron-labs/devtron/pkg/sql" "github.com/devtron-labs/devtron/pkg/team" "github.com/devtron-labs/devtron/pkg/team/read" - repository2 "github.com/devtron-labs/devtron/pkg/team/repository" + repository3 "github.com/devtron-labs/devtron/pkg/team/repository" "github.com/devtron-labs/devtron/pkg/terminal" "github.com/devtron-labs/devtron/pkg/ucid" "github.com/devtron-labs/devtron/pkg/userResource" @@ -190,26 +193,29 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - enforcerImpl, err := casbin.NewEnforcerImpl(syncedEnforcer, casbinSyncedEnforcer, sessionManager, sugaredLogger) + globalAuthorisationConfigRepositoryImpl := repository.NewGlobalAuthorisationConfigRepositoryImpl(sugaredLogger, db) + globalAuthorisationConfigServiceImpl := globalConfig.NewGlobalAuthorisationConfigServiceImpl(sugaredLogger, globalAuthorisationConfigRepositoryImpl) + enforcerImpl, err := casbin.NewEnforcerImpl(syncedEnforcer, casbinSyncedEnforcer, sessionManager, sugaredLogger, globalAuthorisationConfigServiceImpl) if err != nil { return nil, err } - defaultAuthPolicyRepositoryImpl := repository.NewDefaultAuthPolicyRepositoryImpl(db, sugaredLogger) - defaultAuthRoleRepositoryImpl := repository.NewDefaultAuthRoleRepositoryImpl(db, sugaredLogger) - userAuthRepositoryImpl := repository.NewUserAuthRepositoryImpl(db, sugaredLogger, defaultAuthPolicyRepositoryImpl, defaultAuthRoleRepositoryImpl) - userRepositoryImpl := repository.NewUserRepositoryImpl(db, sugaredLogger) - roleGroupRepositoryImpl := repository.NewRoleGroupRepositoryImpl(db, sugaredLogger) - rbacPolicyDataRepositoryImpl := repository.NewRbacPolicyDataRepositoryImpl(sugaredLogger, db) - rbacRoleDataRepositoryImpl := repository.NewRbacRoleDataRepositoryImpl(sugaredLogger, db) - rbacDataCacheFactoryImpl := repository.NewRbacDataCacheFactoryImpl(sugaredLogger, rbacPolicyDataRepositoryImpl, rbacRoleDataRepositoryImpl) + defaultAuthPolicyRepositoryImpl := repository2.NewDefaultAuthPolicyRepositoryImpl(db, sugaredLogger) + defaultAuthRoleRepositoryImpl := repository2.NewDefaultAuthRoleRepositoryImpl(db, sugaredLogger) + userAuthRepositoryImpl := repository2.NewUserAuthRepositoryImpl(db, sugaredLogger, defaultAuthPolicyRepositoryImpl, defaultAuthRoleRepositoryImpl) + userRepositoryImpl := repository2.NewUserRepositoryImpl(db, sugaredLogger) + roleGroupRepositoryImpl := repository2.NewRoleGroupRepositoryImpl(db, sugaredLogger) + rbacPolicyDataRepositoryImpl := repository2.NewRbacPolicyDataRepositoryImpl(sugaredLogger, db) + rbacRoleDataRepositoryImpl := repository2.NewRbacRoleDataRepositoryImpl(sugaredLogger, db) + rbacDataCacheFactoryImpl := repository2.NewRbacDataCacheFactoryImpl(sugaredLogger, rbacPolicyDataRepositoryImpl, rbacRoleDataRepositoryImpl) userCommonServiceImpl, err := user.NewUserCommonServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, sessionManager, rbacDataCacheFactoryImpl) if err != nil { return nil, err } - userAuditRepositoryImpl := repository.NewUserAuditRepositoryImpl(db) + userAuditRepositoryImpl := repository2.NewUserAuditRepositoryImpl(db) userAuditServiceImpl := user.NewUserAuditServiceImpl(sugaredLogger, userAuditRepositoryImpl) roleGroupServiceImpl := user.NewRoleGroupServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, userCommonServiceImpl) - userServiceImpl := user.NewUserServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, sessionManager, userCommonServiceImpl, userAuditServiceImpl, roleGroupServiceImpl) + userAutoAssignGroupMapRepositoryImpl := repository2.NewUserAutoAssignGroupMapRepositoryImpl(db, sugaredLogger) + userServiceImpl := user.NewUserServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, sessionManager, userCommonServiceImpl, userAuditServiceImpl, roleGroupServiceImpl, userAutoAssignGroupMapRepositoryImpl, globalAuthorisationConfigServiceImpl) ssoLoginRepositoryImpl := sso.NewSSOLoginRepositoryImpl(db, sugaredLogger) k8sRuntimeConfig, err := k8s.GetRuntimeConfig() if err != nil { @@ -223,33 +229,33 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - selfRegistrationRolesRepositoryImpl := repository.NewSelfRegistrationRolesRepositoryImpl(db, sugaredLogger) - userSelfRegistrationServiceImpl := user.NewUserSelfRegistrationServiceImpl(sugaredLogger, selfRegistrationRolesRepositoryImpl, userServiceImpl) + selfRegistrationRolesRepositoryImpl := repository2.NewSelfRegistrationRolesRepositoryImpl(db, sugaredLogger) + userSelfRegistrationServiceImpl := user.NewUserSelfRegistrationServiceImpl(sugaredLogger, selfRegistrationRolesRepositoryImpl, userServiceImpl, globalAuthorisationConfigServiceImpl) userAuthOidcHelperImpl, err := authentication.NewUserAuthOidcHelperImpl(sugaredLogger, userSelfRegistrationServiceImpl, dexConfig, settings, sessionManager) if err != nil { return nil, err } - ssoLoginServiceImpl := sso.NewSSOLoginServiceImpl(sugaredLogger, ssoLoginRepositoryImpl, k8sServiceImpl, environmentVariables, userAuthOidcHelperImpl) + ssoLoginServiceImpl := sso.NewSSOLoginServiceImpl(sugaredLogger, ssoLoginRepositoryImpl, k8sServiceImpl, environmentVariables, userAuthOidcHelperImpl, globalAuthorisationConfigServiceImpl) ssoLoginRestHandlerImpl := sso2.NewSsoLoginRestHandlerImpl(validate, sugaredLogger, enforcerImpl, userServiceImpl, ssoLoginServiceImpl) ssoLoginRouterImpl := sso2.NewSsoLoginRouterImpl(ssoLoginRestHandlerImpl) - teamRepositoryImpl := repository2.NewTeamRepositoryImpl(db) + teamRepositoryImpl := repository3.NewTeamRepositoryImpl(db) loginService := middleware.NewUserLogin(sessionManager, k8sClient) userAuthServiceImpl := user.NewUserAuthServiceImpl(userAuthRepositoryImpl, sessionManager, loginService, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, userServiceImpl) teamReadServiceImpl := read.NewTeamReadService(sugaredLogger, teamRepositoryImpl) teamServiceImpl := team.NewTeamServiceImpl(sugaredLogger, teamRepositoryImpl, userAuthServiceImpl, teamReadServiceImpl) - clusterRepositoryImpl := repository3.NewClusterRepositoryImpl(db, sugaredLogger, environmentVariables) + clusterRepositoryImpl := repository4.NewClusterRepositoryImpl(db, sugaredLogger, environmentVariables) syncMap := informer.NewGlobalMapClusterNamespace() k8sInformerFactoryImpl := informer.NewK8sInformerFactoryImpl(sugaredLogger, syncMap, k8sServiceImpl) cronLoggerImpl := cron.NewCronLoggerImpl(sugaredLogger) clusterReadServiceImpl := read2.NewClusterReadServiceImpl(sugaredLogger, clusterRepositoryImpl) runnable := asyncProvider.NewAsyncRunnable(sugaredLogger) - clusterServiceImpl, err := cluster.NewClusterServiceImpl(clusterRepositoryImpl, sugaredLogger, k8sServiceImpl, k8sInformerFactoryImpl, userAuthRepositoryImpl, userRepositoryImpl, roleGroupRepositoryImpl, environmentVariables, cronLoggerImpl, clusterReadServiceImpl, runnable) + clusterServiceImpl, err := cluster.NewClusterServiceImpl(clusterRepositoryImpl, sugaredLogger, k8sServiceImpl, k8sInformerFactoryImpl, userAuthRepositoryImpl, userRepositoryImpl, roleGroupRepositoryImpl, environmentVariables, cronLoggerImpl, clusterReadServiceImpl, runnable, userServiceImpl, globalAuthorisationConfigServiceImpl) if err != nil { return nil, err } appStatusRepositoryImpl := appStatus.NewAppStatusRepositoryImpl(db, sugaredLogger) - environmentRepositoryImpl := repository4.NewEnvironmentRepositoryImpl(db, sugaredLogger, appStatusRepositoryImpl) - attributesRepositoryImpl := repository5.NewAttributesRepositoryImpl(db) + environmentRepositoryImpl := repository5.NewEnvironmentRepositoryImpl(db, sugaredLogger, appStatusRepositoryImpl) + attributesRepositoryImpl := repository6.NewAttributesRepositoryImpl(db) httpClient := util.NewHttpClient() grafanaClientConfig, err := grafana.GetGrafanaClientConfig() if err != nil { @@ -275,7 +281,7 @@ func InitializeApp() (*App, error) { argoCDConfigGetterImpl := config.NewArgoCDConfigGetter(beanConfig, environmentVariables, acdAuthConfig, clusterReadServiceImpl, sugaredLogger, k8sServiceImpl) argoClientWrapperServiceEAImpl := argocdServer.NewArgoClientWrapperServiceEAImpl(sugaredLogger, repositoryCredsK8sClientImpl, argoCDConfigGetterImpl) chartRepositoryServiceImpl := chartRepo.NewChartRepositoryServiceImpl(sugaredLogger, chartRepoRepositoryImpl, k8sServiceImpl, acdAuthConfig, httpClient, serverEnvConfigServerEnvConfig, argoClientWrapperServiceEAImpl, clusterReadServiceImpl) - installedAppRepositoryImpl := repository6.NewInstalledAppRepositoryImpl(sugaredLogger, db) + installedAppRepositoryImpl := repository7.NewInstalledAppRepositoryImpl(sugaredLogger, db) helmClientConfig, err := gRPC.GetConfig() if err != nil { return nil, err @@ -298,7 +304,7 @@ func InitializeApp() (*App, error) { return nil, err } helmAppReadServiceImpl := read4.NewHelmAppReadServiceImpl(sugaredLogger, clusterReadServiceImpl) - installedAppVersionHistoryRepositoryImpl := repository6.NewInstalledAppVersionHistoryRepositoryImpl(sugaredLogger, db) + installedAppVersionHistoryRepositoryImpl := repository7.NewInstalledAppVersionHistoryRepositoryImpl(sugaredLogger, db) repositoryImpl := deploymentConfig.NewRepositoryImpl(db) transactionUtilImpl := sql.NewTransactionUtilImpl(db) chartRepositoryImpl := chartRepoRepository.NewChartRepository(db, transactionUtilImpl) @@ -309,9 +315,9 @@ func InitializeApp() (*App, error) { deploymentConfigServiceImpl := common.NewDeploymentConfigServiceImpl(repositoryImpl, sugaredLogger, chartRepositoryImpl, pipelineRepositoryImpl, appRepositoryImpl, installedAppReadServiceEAImpl, environmentVariables, envConfigOverrideReadServiceImpl, environmentRepositoryImpl, chartRefRepositoryImpl, deploymentConfigReadServiceImpl, acdAuthConfig) installedAppDBServiceImpl := EAMode.NewInstalledAppDBServiceImpl(sugaredLogger, installedAppRepositoryImpl, appRepositoryImpl, userServiceImpl, environmentServiceImpl, installedAppVersionHistoryRepositoryImpl, deploymentConfigServiceImpl) helmAppServiceImpl := service.NewHelmAppServiceImpl(sugaredLogger, clusterServiceImpl, helmAppClientImpl, pumpImpl, enforcerUtilHelmImpl, serverDataStoreServerDataStore, serverEnvConfigServerEnvConfig, appStoreApplicationVersionRepositoryImpl, environmentServiceImpl, pipelineRepositoryImpl, installedAppRepositoryImpl, appRepositoryImpl, clusterRepositoryImpl, k8sServiceImpl, helmReleaseConfig, helmAppReadServiceImpl, clusterReadServiceImpl, installedAppDBServiceImpl) - dockerArtifactStoreRepositoryImpl := repository7.NewDockerArtifactStoreRepositoryImpl(db, environmentVariables) - dockerRegistryIpsConfigRepositoryImpl := repository7.NewDockerRegistryIpsConfigRepositoryImpl(db) - ociRegistryConfigRepositoryImpl := repository7.NewOCIRegistryConfigRepositoryImpl(db) + dockerArtifactStoreRepositoryImpl := repository8.NewDockerArtifactStoreRepositoryImpl(db, environmentVariables) + dockerRegistryIpsConfigRepositoryImpl := repository8.NewDockerRegistryIpsConfigRepositoryImpl(db) + ociRegistryConfigRepositoryImpl := repository8.NewOCIRegistryConfigRepositoryImpl(db) dockerRegistryConfigImpl := pipeline.NewDockerRegistryConfigImpl(sugaredLogger, helmAppServiceImpl, dockerArtifactStoreRepositoryImpl, dockerRegistryIpsConfigRepositoryImpl, ociRegistryConfigRepositoryImpl, argoClientWrapperServiceEAImpl) deleteServiceImpl := delete2.NewDeleteServiceImpl(sugaredLogger, teamServiceImpl, clusterServiceImpl, environmentServiceImpl, chartRepositoryServiceImpl, installedAppRepositoryImpl, dockerRegistryConfigImpl, dockerArtifactStoreRepositoryImpl, k8sInformerFactoryImpl, k8sServiceImpl, appRepositoryImpl) teamRestHandlerImpl := team2.NewTeamRestHandlerImpl(sugaredLogger, teamServiceImpl, userServiceImpl, enforcerImpl, validate, userAuthServiceImpl, deleteServiceImpl) @@ -328,11 +334,11 @@ func InitializeApp() (*App, error) { commonBaseServiceImpl := commonService.NewCommonBaseServiceImpl(sugaredLogger, environmentVariables, moduleReadServiceImpl) commonRestHandlerImpl := restHandler.NewCommonRestHandlerImpl(sugaredLogger, userServiceImpl, commonBaseServiceImpl) commonRouterImpl := router.NewCommonRouterImpl(commonRestHandlerImpl) - genericNoteRepositoryImpl := repository8.NewGenericNoteRepositoryImpl(db, transactionUtilImpl) - genericNoteHistoryRepositoryImpl := repository8.NewGenericNoteHistoryRepositoryImpl(db, transactionUtilImpl) + genericNoteRepositoryImpl := repository9.NewGenericNoteRepositoryImpl(db, transactionUtilImpl) + genericNoteHistoryRepositoryImpl := repository9.NewGenericNoteHistoryRepositoryImpl(db, transactionUtilImpl) genericNoteHistoryServiceImpl := genericNotes.NewGenericNoteHistoryServiceImpl(genericNoteHistoryRepositoryImpl, sugaredLogger) genericNoteServiceImpl := genericNotes.NewGenericNoteServiceImpl(genericNoteRepositoryImpl, genericNoteHistoryServiceImpl, userRepositoryImpl, sugaredLogger) - clusterDescriptionRepositoryImpl := repository3.NewClusterDescriptionRepositoryImpl(db, sugaredLogger) + clusterDescriptionRepositoryImpl := repository4.NewClusterDescriptionRepositoryImpl(db, sugaredLogger) clusterDescriptionServiceImpl := cluster.NewClusterDescriptionServiceImpl(clusterDescriptionRepositoryImpl, userRepositoryImpl, sugaredLogger) clusterRbacServiceImpl := rbac2.NewClusterRbacServiceImpl(environmentServiceImpl, enforcerImpl, enforcerUtilImpl, clusterServiceImpl, sugaredLogger, userServiceImpl, clusterReadServiceImpl) clusterRestHandlerImpl := cluster2.NewClusterRestHandlerImpl(clusterServiceImpl, genericNoteServiceImpl, clusterDescriptionServiceImpl, sugaredLogger, userServiceImpl, validate, enforcerImpl, deleteServiceImpl, environmentServiceImpl, clusterRbacServiceImpl) @@ -345,7 +351,7 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - gitOpsConfigRepositoryImpl := repository5.NewGitOpsConfigRepositoryImpl(sugaredLogger, db, environmentVariables) + gitOpsConfigRepositoryImpl := repository6.NewGitOpsConfigRepositoryImpl(sugaredLogger, db, environmentVariables) gitOpsConfigReadServiceImpl := config2.NewGitOpsConfigReadServiceImpl(sugaredLogger, gitOpsConfigRepositoryImpl, userServiceImpl, environmentVariables, moduleReadServiceImpl) deploymentTypeOverrideServiceImpl := providerConfig.NewDeploymentTypeOverrideServiceImpl(sugaredLogger, environmentVariables, attributesServiceImpl) chartTemplateServiceImpl := util.NewChartTemplateServiceImpl(sugaredLogger) @@ -353,7 +359,7 @@ func InitializeApp() (*App, error) { eaModeDeploymentServiceImpl := deployment.NewEAModeDeploymentServiceImpl(sugaredLogger, helmAppServiceImpl, appStoreApplicationVersionRepositoryImpl, helmAppClientImpl, installedAppRepositoryImpl, ociRegistryConfigRepositoryImpl, appStoreDeploymentCommonServiceImpl, helmAppReadServiceImpl) appStoreValidatorImpl := service2.NewAppAppStoreValidatorImpl(sugaredLogger) appStoreDeploymentDBServiceImpl := service2.NewAppStoreDeploymentDBServiceImpl(sugaredLogger, installedAppRepositoryImpl, appStoreApplicationVersionRepositoryImpl, appRepositoryImpl, environmentServiceImpl, installedAppVersionHistoryRepositoryImpl, environmentVariables, gitOpsConfigReadServiceImpl, deploymentTypeOverrideServiceImpl, eaModeDeploymentServiceImpl, appStoreValidatorImpl, installedAppDBServiceImpl, deploymentConfigServiceImpl, clusterReadServiceImpl) - chartGroupDeploymentRepositoryImpl := repository9.NewChartGroupDeploymentRepositoryImpl(db, sugaredLogger) + chartGroupDeploymentRepositoryImpl := repository10.NewChartGroupDeploymentRepositoryImpl(db, sugaredLogger) acdConfig, err := argocdServer.GetACDDeploymentConfig() if err != nil { return nil, err @@ -361,11 +367,11 @@ func InitializeApp() (*App, error) { deletePostProcessorImpl := service2.NewDeletePostProcessorImpl(sugaredLogger) appStoreDeploymentServiceImpl := service2.NewAppStoreDeploymentServiceImpl(sugaredLogger, installedAppRepositoryImpl, installedAppDBServiceImpl, appStoreDeploymentDBServiceImpl, chartGroupDeploymentRepositoryImpl, appStoreApplicationVersionRepositoryImpl, appRepositoryImpl, eaModeDeploymentServiceImpl, eaModeDeploymentServiceImpl, eaModeDeploymentServiceImpl, environmentServiceImpl, helmAppServiceImpl, installedAppVersionHistoryRepositoryImpl, environmentVariables, acdConfig, gitOpsConfigReadServiceImpl, deletePostProcessorImpl, appStoreValidatorImpl, deploymentConfigServiceImpl, ociRegistryConfigRepositoryImpl) fluxApplicationServiceImpl := fluxApplication.NewFluxApplicationServiceImpl(sugaredLogger, helmAppReadServiceImpl, clusterServiceImpl, helmAppClientImpl, pumpImpl, pipelineRepositoryImpl, installedAppRepositoryImpl) - k8sResourceHistoryRepositoryImpl := repository10.NewK8sResourceHistoryRepositoryImpl(db, sugaredLogger) + k8sResourceHistoryRepositoryImpl := repository11.NewK8sResourceHistoryRepositoryImpl(db, sugaredLogger) k8sResourceHistoryServiceImpl := kubernetesResourceAuditLogs.Newk8sResourceHistoryServiceImpl(k8sResourceHistoryRepositoryImpl, sugaredLogger, appRepositoryImpl, environmentRepositoryImpl) argoApplicationConfigServiceImpl := config3.NewArgoApplicationConfigServiceImpl(sugaredLogger, k8sServiceImpl, clusterRepositoryImpl) k8sCommonServiceImpl := k8s2.NewK8sCommonServiceImpl(sugaredLogger, k8sServiceImpl, argoApplicationConfigServiceImpl, clusterReadServiceImpl, runnable) - ephemeralContainersRepositoryImpl := repository3.NewEphemeralContainersRepositoryImpl(db, transactionUtilImpl) + ephemeralContainersRepositoryImpl := repository4.NewEphemeralContainersRepositoryImpl(db, transactionUtilImpl) ephemeralContainerServiceImpl := cluster.NewEphemeralContainerServiceImpl(ephemeralContainersRepositoryImpl, sugaredLogger) terminalSessionHandlerImpl := terminal.NewTerminalSessionHandlerImpl(environmentServiceImpl, sugaredLogger, k8sServiceImpl, ephemeralContainerServiceImpl, argoApplicationConfigServiceImpl, clusterReadServiceImpl, runnable) k8sApplicationServiceImpl, err := application.NewK8sApplicationServiceImpl(sugaredLogger, clusterServiceImpl, pumpImpl, helmAppServiceImpl, k8sServiceImpl, acdAuthConfig, k8sResourceHistoryServiceImpl, k8sCommonServiceImpl, terminalSessionHandlerImpl, ephemeralContainerServiceImpl, ephemeralContainersRepositoryImpl, fluxApplicationServiceImpl, clusterReadServiceImpl) @@ -403,7 +409,7 @@ func InitializeApp() (*App, error) { } serviceImpl := ucid.NewServiceImpl(sugaredLogger, k8sServiceImpl, acdAuthConfig) providerIdentifierServiceImpl := providerIdentifier.NewProviderIdentifierServiceImpl(sugaredLogger) - userAttributesRepositoryImpl := repository5.NewUserAttributesRepositoryImpl(db) + userAttributesRepositoryImpl := repository6.NewUserAttributesRepositoryImpl(db) telemetryEventClientImpl, err := telemetry2.NewTelemetryEventClientImpl(sugaredLogger, httpClient, clusterServiceImpl, k8sServiceImpl, acdAuthConfig, userServiceImpl, attributesRepositoryImpl, ssoLoginServiceImpl, posthogClient, serviceImpl, moduleRepositoryImpl, serverDataStoreServerDataStore, userAuditServiceImpl, helmAppClientImpl, providerIdentifierServiceImpl, cronLoggerImpl, installedAppReadServiceEAImpl, environmentVariables, userAttributesRepositoryImpl) if err != nil { return nil, err @@ -438,7 +444,7 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - scanToolMetadataRepositoryImpl := repository11.NewScanToolMetadataRepositoryImpl(db, sugaredLogger) + scanToolMetadataRepositoryImpl := repository12.NewScanToolMetadataRepositoryImpl(db, sugaredLogger) scanToolMetadataServiceImpl := scanTool.NewScanToolMetadataServiceImpl(sugaredLogger, scanToolMetadataRepositoryImpl) moduleServiceImpl := module.NewModuleServiceImpl(sugaredLogger, serverEnvConfigServerEnvConfig, moduleRepositoryImpl, moduleActionAuditLogRepositoryImpl, helmAppServiceImpl, serverDataStoreServerDataStore, serverCacheServiceImpl, moduleCacheServiceImpl, moduleCronServiceImpl, moduleServiceHelperImpl, moduleResourceStatusRepositoryImpl, scanToolMetadataServiceImpl, environmentVariables, moduleEnvConfig) moduleRestHandlerImpl := module2.NewModuleRestHandlerImpl(sugaredLogger, moduleServiceImpl, userServiceImpl, enforcerImpl, validate) @@ -459,7 +465,8 @@ func InitializeApp() (*App, error) { apiTokenRestHandlerImpl := apiToken2.NewApiTokenRestHandlerImpl(sugaredLogger, apiTokenServiceImpl, userServiceImpl, enforcerImpl, validate) apiTokenRouterImpl := apiToken2.NewApiTokenRouterImpl(apiTokenRestHandlerImpl) k8sCapacityServiceImpl := capacity.NewK8sCapacityServiceImpl(sugaredLogger, k8sApplicationServiceImpl, k8sServiceImpl, k8sCommonServiceImpl) - k8sCapacityRestHandlerImpl := capacity2.NewK8sCapacityRestHandlerImpl(sugaredLogger, k8sCapacityServiceImpl, userServiceImpl, enforcerImpl, clusterServiceImpl, environmentServiceImpl, clusterRbacServiceImpl, clusterReadServiceImpl, validate) + clusterCacheServiceImpl := cache.NewClusterCacheServiceImpl(sugaredLogger) + k8sCapacityRestHandlerImpl := capacity2.NewK8sCapacityRestHandlerImpl(sugaredLogger, k8sCapacityServiceImpl, userServiceImpl, enforcerImpl, clusterServiceImpl, environmentServiceImpl, clusterRbacServiceImpl, clusterReadServiceImpl, validate, clusterCacheServiceImpl) k8sCapacityRouterImpl := capacity2.NewK8sCapacityRouterImpl(k8sCapacityRestHandlerImpl) webhookHelmServiceImpl := webhookHelm.NewWebhookHelmServiceImpl(sugaredLogger, helmAppServiceImpl, clusterServiceImpl, chartRepositoryServiceImpl, attributesServiceImpl) webhookHelmRestHandlerImpl := webhookHelm2.NewWebhookHelmRestHandlerImpl(sugaredLogger, webhookHelmServiceImpl, userServiceImpl, enforcerImpl, validate) @@ -469,7 +476,7 @@ func InitializeApp() (*App, error) { userAttributesRouterImpl := router.NewUserAttributesRouterImpl(userAttributesRestHandlerImpl) telemetryRestHandlerImpl := restHandler.NewTelemetryRestHandlerImpl(sugaredLogger, telemetryEventClientImpl, enforcerImpl, userServiceImpl) telemetryRouterImpl := router.NewTelemetryRouterImpl(sugaredLogger, telemetryRestHandlerImpl) - terminalAccessRepositoryImpl := repository5.NewTerminalAccessRepositoryImpl(db, sugaredLogger) + terminalAccessRepositoryImpl := repository6.NewTerminalAccessRepositoryImpl(db, sugaredLogger) userTerminalSessionConfig, err := clusterTerminalAccess.GetTerminalAccessConfig() if err != nil { return nil, err @@ -487,7 +494,7 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - materialRepositoryImpl := repository12.NewMaterialRepositoryImpl(db) + materialRepositoryImpl := repository13.NewMaterialRepositoryImpl(db) gitMaterialReadServiceImpl := read10.NewGitMaterialReadServiceImpl(sugaredLogger, materialRepositoryImpl) appCrudOperationServiceImpl := app2.NewAppCrudOperationServiceImpl(appLabelRepositoryImpl, sugaredLogger, appRepositoryImpl, userRepositoryImpl, installedAppRepositoryImpl, genericNoteServiceImpl, installedAppDBServiceImpl, crudOperationServiceConfig, dbMigrationServiceImpl, gitMaterialReadServiceImpl) appInfoRestHandlerImpl := appInfo.NewAppInfoRestHandlerImpl(sugaredLogger, appCrudOperationServiceImpl, userServiceImpl, validate, enforcerUtilImpl, enforcerImpl, helmAppServiceImpl, enforcerUtilHelmImpl, genericNoteServiceImpl, commonEnforcementUtilImpl) @@ -505,7 +512,6 @@ func InitializeApp() (*App, error) { userResourceServiceImpl := userResource.NewUserResourceServiceImpl(sugaredLogger, teamServiceImpl, environmentServiceImpl, clusterServiceImpl, k8sApplicationServiceImpl, enforcerUtilImpl, commonEnforcementUtilImpl, enforcerImpl, appCrudOperationServiceImpl) restHandlerImpl := userResource2.NewUserResourceRestHandler(sugaredLogger, userServiceImpl, userResourceServiceImpl) routerImpl := userResource2.NewUserResourceRouterImpl(restHandlerImpl) - clusterCacheServiceImpl := cache.NewClusterCacheServiceImpl(sugaredLogger) clusterOverviewConfig, err := config4.GetClusterOverviewConfig() if err != nil { return nil, err @@ -513,7 +519,9 @@ func InitializeApp() (*App, error) { clusterOverviewServiceImpl := overview.NewClusterOverviewServiceImpl(sugaredLogger, clusterServiceImpl, k8sCapacityServiceImpl, clusterCacheServiceImpl, k8sCommonServiceImpl, enforcerImpl, clusterOverviewConfig) infraOverviewRestHandlerImpl := restHandler.NewInfraOverviewRestHandlerImpl(sugaredLogger, clusterOverviewServiceImpl, clusterCacheServiceImpl, userServiceImpl, validate, enforcerImpl) infraOverviewRouterImpl := router.NewInfraOverviewRouterImpl(infraOverviewRestHandlerImpl) - muxRouter := NewMuxRouter(sugaredLogger, ssoLoginRouterImpl, teamRouterImpl, userAuthRouterImpl, userRouterImpl, commonRouterImpl, clusterRouterImpl, dashboardRouterImpl, helmAppRouterImpl, environmentRouterImpl, k8sApplicationRouterImpl, chartRepositoryRouterImpl, appStoreDiscoverRouterImpl, appStoreValuesRouterImpl, appStoreDeploymentRouterImpl, chartProviderRouterImpl, dockerRegRouterImpl, dashboardTelemetryRouterImpl, commonDeploymentRouterImpl, externalLinkRouterImpl, moduleRouterImpl, serverRouterImpl, apiTokenRouterImpl, k8sCapacityRouterImpl, webhookHelmRouterImpl, userAttributesRouterImpl, telemetryRouterImpl, userTerminalAccessRouterImpl, attributesRouterImpl, appRouterEAModeImpl, rbacRoleRouterImpl, argoApplicationRouterImpl, fluxApplicationRouterImpl, routerImpl, infraOverviewRouterImpl) + authorisationConfigRestHandlerImpl := globalConfig2.NewGlobalAuthorisationConfigRestHandlerImpl(validate, sugaredLogger, enforcerImpl, userServiceImpl, globalAuthorisationConfigServiceImpl, userCommonServiceImpl, commonEnforcementUtilImpl) + authorisationConfigRouterImpl := globalConfig2.NewGlobalConfigAuthorisationRouterImpl(authorisationConfigRestHandlerImpl) + muxRouter := NewMuxRouter(sugaredLogger, ssoLoginRouterImpl, teamRouterImpl, userAuthRouterImpl, userRouterImpl, commonRouterImpl, clusterRouterImpl, dashboardRouterImpl, helmAppRouterImpl, environmentRouterImpl, k8sApplicationRouterImpl, chartRepositoryRouterImpl, appStoreDiscoverRouterImpl, appStoreValuesRouterImpl, appStoreDeploymentRouterImpl, chartProviderRouterImpl, dockerRegRouterImpl, dashboardTelemetryRouterImpl, commonDeploymentRouterImpl, externalLinkRouterImpl, moduleRouterImpl, serverRouterImpl, apiTokenRouterImpl, k8sCapacityRouterImpl, webhookHelmRouterImpl, userAttributesRouterImpl, telemetryRouterImpl, userTerminalAccessRouterImpl, attributesRouterImpl, appRouterEAModeImpl, rbacRoleRouterImpl, argoApplicationRouterImpl, fluxApplicationRouterImpl, routerImpl, infraOverviewRouterImpl, authorisationConfigRouterImpl) mainApp := NewApp(db, sessionManager, muxRouter, telemetryEventClientImpl, posthogClient, sugaredLogger, userServiceImpl) return mainApp, nil } diff --git a/pkg/auth/authorisation/casbin/rbac.go b/pkg/auth/authorisation/casbin/rbac.go index 51df9c694c..d62bd3e1d8 100644 --- a/pkg/auth/authorisation/casbin/rbac.go +++ b/pkg/auth/authorisation/casbin/rbac.go @@ -509,7 +509,10 @@ func (e *EnforcerImpl) getSubjectsFromToken(tokenString string) ([]string, bool) if email == "" { return nil, true } - subjects := []string{email} + subjects := make([]string, 0) + if e.globalAuthorisationConfigService.IsDevtronSystemManagedConfigActive() || util3.CheckIfAdminOrApiToken(email) { + subjects = append(subjects, email) + } if e.globalAuthorisationConfigService != nil && e.globalAuthorisationConfigService.IsGroupClaimsConfigActive() && !util3.CheckIfAdminOrApiToken(email) { diff --git a/pkg/auth/user/UserService.go b/pkg/auth/user/UserService.go index 8bca888faf..21320edd23 100644 --- a/pkg/auth/user/UserService.go +++ b/pkg/auth/user/UserService.go @@ -83,7 +83,7 @@ type UserService interface { GetEmailAndGroupClaimsFromToken(token string) (string, []string, error) SyncOrchestratorToCasbin() (bool, error) GetUserByToken(context context.Context, token string) (int32, string, error) - //IsSuperAdmin(userId int) (bool, error) + IsSuperAdmin(userId int, token string) (bool, error) GetByIdIncludeDeleted(id int32) (*userBean.UserInfo, error) UserExists(emailId string) bool UpdateTriggerPolicyForTerminalAccess() (err error) @@ -1627,10 +1627,10 @@ func (impl *UserServiceImpl) SyncOrchestratorToCasbin() (bool, error) { return true, nil } -func (impl *UserServiceImpl) IsSuperAdmin(userId int) (bool, error) { +func (impl *UserServiceImpl) IsSuperAdmin(userId int, token string) (bool, error) { //validating if action user is not admin and trying to update user who has super admin polices, return 403 isSuperAdmin := false - userCasbinRoles, err := impl.CheckUserRoles(int32(userId), "") + userCasbinRoles, err := impl.CheckUserRoles(int32(userId), token) if err != nil { return isSuperAdmin, err } diff --git a/pkg/auth/user/UserService_ent.go b/pkg/auth/user/UserService_ent.go index 9029b2cd2d..f576f49da3 100644 --- a/pkg/auth/user/UserService_ent.go +++ b/pkg/auth/user/UserService_ent.go @@ -58,6 +58,22 @@ func (impl *UserServiceImpl) assignUserGroups(tx *pg.Tx, userInfo *userBean.User } func (impl *UserServiceImpl) checkAndPerformOperationsForGroupClaims(tx *pg.Tx, userInfo *userBean.UserInfo) (bool, error) { + isGroupClaimsActive := impl.globalAuthorisationConfigService.IsGroupClaimsConfigActive() + isSystemManagedActive := impl.globalAuthorisationConfigService.IsDevtronSystemManagedConfigActive() + if !isSystemManagedActive && isGroupClaimsActive { + // In group-claims-only mode, new users must not get direct casbin permissions; + // their access is governed entirely by JWT group claims resolved at login. + userInfo.RoleFilters = []userBean.RoleFilter{} + userInfo.Groups = []string{} + userInfo.UserRoleGroup = []userBean.UserRoleGroup{} + userInfo.SuperAdmin = false + err := tx.Commit() + if err != nil { + impl.logger.Errorw("error encountered in checkAndPerformOperationsForGroupClaims", "err", err) + return false, err + } + return true, nil + } return false, nil } @@ -122,26 +138,31 @@ func (impl *UserServiceImpl) CheckUserRoles(id int32, token string) ([]string, e return nil, err } + isGroupClaimsActive := impl.globalAuthorisationConfigService.IsGroupClaimsConfigActive() + isDevtronSystemActive := impl.globalAuthorisationConfigService.IsDevtronSystemManagedConfigActive() var groups []string // devtron-system-managed path: get roles from casbin directly - activeRoles, err := casbin2.GetRolesForUser(model.EmailId) - if err != nil { - impl.logger.Errorw("No Roles Found for user", "id", model.Id) - return nil, err - } - groups = append(groups, activeRoles...) - if len(groups) > 0 { - // getting unique, handling for duplicate roles - roleFromGroups, err := impl.getUniquesRolesByGroupCasbinNames(groups) + // skipped when group-claims-only mode is active (to avoid stale casbin policies leaking permissions) + if isDevtronSystemActive || util3.CheckIfAdminOrApiToken(model.EmailId) { + activeRoles, err := casbin2.GetRolesForUser(model.EmailId) if err != nil { - impl.logger.Errorw("error in getUniquesRolesByGroupCasbinNames", "err", err) + impl.logger.Errorw("No Roles Found for user", "id", model.Id) return nil, err } - groups = append(groups, roleFromGroups...) + groups = append(groups, activeRoles...) + if len(groups) > 0 { + // getting unique, handling for duplicate roles + roleFromGroups, err := impl.getUniquesRolesByGroupCasbinNames(groups) + if err != nil { + impl.logger.Errorw("error in getUniquesRolesByGroupCasbinNames", "err", err) + return nil, err + } + groups = append(groups, roleFromGroups...) + } } // group claims path: check group claims active and add role groups from JWT claims - isGroupClaimsActive := impl.globalAuthorisationConfigService.IsGroupClaimsConfigActive() + // skipping for api token; also skipping for empty token (update user super-admin check — target user token unavailable) if isGroupClaimsActive && !strings.HasPrefix(model.EmailId, userBean.API_TOKEN_USER_EMAIL_PREFIX) { _, groupClaims, err := impl.GetEmailAndGroupClaimsFromToken(token) if err != nil { @@ -345,8 +366,12 @@ func (impl *UserServiceImpl) GetEmailAndGroupClaimsFromToken(token string) (stri if err != nil { return "", nil, err } + groupsClaims := make([]string, 0) email, groups := impl.globalAuthorisationConfigService.GetEmailAndGroupsFromClaims(mapClaims) - return email, groups, nil + if impl.globalAuthorisationConfigService.IsGroupClaimsConfigActive() { + groupsClaims = groups + } + return email, groupsClaims, nil } func (impl *UserServiceImpl) getMapClaims(token string) (jwtv4.MapClaims, error) { @@ -450,7 +475,7 @@ func getApproverFromRoleFilter(roleFilter userBean.RoleFilter) bool { func (impl *UserServiceImpl) checkValidationAndPerformOperationsForUpdate(token string, tx *pg.Tx, model *userrepo.UserModel, userInfo *userBean.UserInfo, userGroupsUpdated bool, timeoutWindowConfigId int) (operationCompleted bool, isUserSuperAdmin bool, err error) { //validating if action user is not admin and trying to update user who has super admin polices, return 403 // isUserSuperAdminOrManageAllAccess only super-admin is checked as manage all access is not applicable for user - isUserSuperAdmin, err = impl.IsSuperAdmin(int(userInfo.Id)) + isUserSuperAdmin, err = impl.IsSuperAdmin(int(userInfo.Id), token) if err != nil { return false, isUserSuperAdmin, err } diff --git a/pkg/auth/user/mocks/UserService.go b/pkg/auth/user/mocks/UserService.go index 43b2924118..a513016c7e 100644 --- a/pkg/auth/user/mocks/UserService.go +++ b/pkg/auth/user/mocks/UserService.go @@ -218,18 +218,18 @@ func (mr *MockUserServiceMockRecorder) GetUserByToken(context, token interface{} } // IsSuperAdmin mocks base method. -func (m *MockUserService) IsSuperAdmin(userId int) (bool, error) { +func (m *MockUserService) IsSuperAdmin(userId int, token string) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsSuperAdmin", userId) + ret := m.ctrl.Call(m, "IsSuperAdmin", userId, token) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } // IsSuperAdmin indicates an expected call of IsSuperAdmin. -func (mr *MockUserServiceMockRecorder) IsSuperAdmin(userId interface{}) *gomock.Call { +func (mr *MockUserServiceMockRecorder) IsSuperAdmin(userId, token interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSuperAdmin", reflect.TypeOf((*MockUserService)(nil).IsSuperAdmin), userId) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSuperAdmin", reflect.TypeOf((*MockUserService)(nil).IsSuperAdmin), userId, token) } // SaveLoginAudit mocks base method. diff --git a/pkg/cluster/ClusterService.go b/pkg/cluster/ClusterService.go index 0bd534bc1a..afd624e640 100644 --- a/pkg/cluster/ClusterService.go +++ b/pkg/cluster/ClusterService.go @@ -43,7 +43,10 @@ import ( "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/devtron-labs/common-lib/utils/k8s" casbin2 "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin" + globalConfig "github.com/devtron-labs/devtron/pkg/auth/authorisation/globalConfig" + "github.com/devtron-labs/devtron/pkg/auth/user" repository3 "github.com/devtron-labs/devtron/pkg/auth/user/repository" + util3 "github.com/devtron-labs/devtron/pkg/auth/user/util" "github.com/devtron-labs/devtron/pkg/k8s/informer" customErr "github.com/juju/errors" k8sError "k8s.io/apimachinery/pkg/api/errors" @@ -85,9 +88,9 @@ type ClusterService interface { FindAllForAutoComplete() ([]bean.ClusterBean, error) CreateGrafanaDataSource(clusterBean *bean.ClusterBean, env *repository2.Environment) (int, error) GetAllClusterNamespaces() map[string][]string - FindAllNamespacesByUserIdAndClusterId(userId int32, clusterId int, isActionUserSuperAdmin bool) ([]string, error) - FindAllForClusterByUserId(userId int32, isActionUserSuperAdmin bool) ([]bean.ClusterBean, error) - FetchRolesFromGroup(userId int32) ([]*repository3.RoleModel, error) + FindAllNamespacesByUserIdAndClusterId(userId int32, clusterId int, isActionUserSuperAdmin bool, token string) ([]string, error) + FindAllForClusterByUserId(userId int32, isActionUserSuperAdmin bool, token string) ([]bean.ClusterBean, error) + FetchRolesFromGroup(userId int32, token string) ([]*repository3.RoleModel, error) HandleErrorInClusterConnections(clusters []*bean.ClusterBean, respMap *sync.Map, clusterExistInDb bool) ConnectClustersInBatch(clusters []*bean.ClusterBean, clusterExistInDb bool) ConvertClusterBeanToCluster(clusterBean *bean.ClusterBean, userId int32) *repository.Cluster @@ -98,15 +101,17 @@ type ClusterService interface { } type ClusterServiceImpl struct { - clusterRepository repository.ClusterRepository - logger *zap.SugaredLogger - K8sUtil *k8s.K8sServiceImpl - K8sInformerFactory informer.K8sInformerFactory - userAuthRepository repository3.UserAuthRepository - userRepository repository3.UserRepository - roleGroupRepository repository3.RoleGroupRepository - clusterReadService read.ClusterReadService - asyncRunnable *async.Runnable + clusterRepository repository.ClusterRepository + logger *zap.SugaredLogger + K8sUtil *k8s.K8sServiceImpl + K8sInformerFactory informer.K8sInformerFactory + userAuthRepository repository3.UserAuthRepository + userRepository repository3.UserRepository + roleGroupRepository repository3.RoleGroupRepository + clusterReadService read.ClusterReadService + asyncRunnable *async.Runnable + userService user.UserService + globalAuthorisationConfigService globalConfig.GlobalAuthorisationConfigService } func NewClusterServiceImpl(repository repository.ClusterRepository, logger *zap.SugaredLogger, @@ -116,17 +121,21 @@ func NewClusterServiceImpl(repository repository.ClusterRepository, logger *zap. envVariables *globalUtil.EnvironmentVariables, cronLogger *cronUtil.CronLoggerImpl, clusterReadService read.ClusterReadService, - asyncRunnable *async.Runnable) (*ClusterServiceImpl, error) { + asyncRunnable *async.Runnable, + userService user.UserService, + globalAuthorisationConfigService globalConfig.GlobalAuthorisationConfigService) (*ClusterServiceImpl, error) { clusterService := &ClusterServiceImpl{ - clusterRepository: repository, - logger: logger, - K8sUtil: K8sUtil, - K8sInformerFactory: K8sInformerFactory, - userAuthRepository: userAuthRepository, - userRepository: userRepository, - roleGroupRepository: roleGroupRepository, - clusterReadService: clusterReadService, - asyncRunnable: asyncRunnable, + clusterRepository: repository, + logger: logger, + K8sUtil: K8sUtil, + K8sInformerFactory: K8sInformerFactory, + userAuthRepository: userAuthRepository, + userRepository: userRepository, + roleGroupRepository: roleGroupRepository, + clusterReadService: clusterReadService, + asyncRunnable: asyncRunnable, + userService: userService, + globalAuthorisationConfigService: globalAuthorisationConfigService, } // initialise cron newCron := cron.New(cron.WithChain(cron.Recover(cronLogger))) @@ -673,7 +682,7 @@ const ( ErrClusterNotReachable = "cluster is not reachable" ) -func (impl *ClusterServiceImpl) FindAllNamespacesByUserIdAndClusterId(userId int32, clusterId int, isActionUserSuperAdmin bool) ([]string, error) { +func (impl *ClusterServiceImpl) FindAllNamespacesByUserIdAndClusterId(userId int32, clusterId int, isActionUserSuperAdmin bool, token string) ([]string, error) { result := make([]string, 0) clusterBean, err := impl.clusterReadService.FindById(clusterId) if err != nil { @@ -703,7 +712,7 @@ func (impl *ClusterServiceImpl) FindAllNamespacesByUserIdAndClusterId(userId int } } } else { - roles, err := impl.FetchRolesFromGroup(userId) + roles, err := impl.FetchRolesFromGroup(userId, token) if err != nil { impl.logger.Errorw("error on fetching user roles for cluster list", "err", err) return nil, err @@ -731,12 +740,12 @@ func (impl *ClusterServiceImpl) FindAllNamespacesByUserIdAndClusterId(userId int return result, nil } -func (impl *ClusterServiceImpl) FindAllForClusterByUserId(userId int32, isActionUserSuperAdmin bool) ([]bean.ClusterBean, error) { +func (impl *ClusterServiceImpl) FindAllForClusterByUserId(userId int32, isActionUserSuperAdmin bool, token string) ([]bean.ClusterBean, error) { if isActionUserSuperAdmin { return impl.FindAllForAutoComplete() } allowedClustersMap := make(map[string]bool) - roles, err := impl.FetchRolesFromGroup(userId) + roles, err := impl.FetchRolesFromGroup(userId, token) if err != nil { impl.logger.Errorw("error while fetching user roles from db", "error", err) return nil, err @@ -763,16 +772,36 @@ func (impl *ClusterServiceImpl) FindAllForClusterByUserId(userId int32, isAction return beans, nil } -func (impl *ClusterServiceImpl) FetchRolesFromGroup(userId int32) ([]*repository3.RoleModel, error) { - user, err := impl.userRepository.GetByIdIncludeDeleted(userId) +func (impl *ClusterServiceImpl) FetchRolesFromGroup(userId int32, token string) ([]*repository3.RoleModel, error) { + userModel, err := impl.userRepository.GetByIdIncludeDeleted(userId) if err != nil { impl.logger.Errorw("error while fetching user from db", "error", err) return nil, err } - groups, err := casbin2.GetRolesForUser(user.EmailId) - if err != nil { - impl.logger.Errorw("No Roles Found for user", "id", user.Id) - return nil, err + isGroupClaimsActive := impl.globalAuthorisationConfigService != nil && impl.globalAuthorisationConfigService.IsGroupClaimsConfigActive() + isDevtronSystemActive := impl.globalAuthorisationConfigService != nil && impl.globalAuthorisationConfigService.IsDevtronSystemManagedConfigActive() + var groups []string + // devtron-system-managed path: get roles from casbin directly + // skipped when group-claims-only mode is active (to avoid stale casbin policies leaking permissions) + if isDevtronSystemActive || util3.CheckIfAdminOrApiToken(userModel.EmailId) { + casbinGroups, err := casbin2.GetRolesForUser(userModel.EmailId) + if err != nil { + impl.logger.Errorw("No Roles Found for user", "id", userModel.Id) + return nil, err + } + groups = append(groups, casbinGroups...) + } + // group claims path: append group casbin names from JWT token if group claims active + if isGroupClaimsActive { + _, groupClaims, err := impl.userService.GetEmailAndGroupClaimsFromToken(token) + if err != nil { + impl.logger.Errorw("error in GetEmailAndGroupClaimsFromToken", "err", err) + return nil, err + } + if len(groupClaims) > 0 { + groupsCasbinNames := util3.GetGroupCasbinName(groupClaims) + groups = append(groups, groupsCasbinNames...) + } } roleEntity := "cluster" roles, err := impl.userAuthRepository.GetRolesByUserIdAndEntityType(userId, roleEntity) @@ -780,13 +809,15 @@ func (impl *ClusterServiceImpl) FetchRolesFromGroup(userId int32) ([]*repository impl.logger.Errorw("error on fetching user roles for cluster list", "err", err) return nil, err } - rolesFromGroup, err := impl.roleGroupRepository.GetRolesByGroupNamesAndEntity(groups, roleEntity) - if err != nil && err != pg.ErrNoRows { - impl.logger.Errorw("error in getting roles by group names", "err", err) - return nil, err - } - if len(rolesFromGroup) > 0 { - roles = append(roles, rolesFromGroup...) + if len(groups) > 0 { + rolesFromGroup, err := impl.roleGroupRepository.GetRolesByGroupNamesAndEntity(groups, roleEntity) + if err != nil && err != pg.ErrNoRows { + impl.logger.Errorw("error in getting roles by group names", "err", err) + return nil, err + } + if len(rolesFromGroup) > 0 { + roles = append(roles, rolesFromGroup...) + } } return roles, nil } diff --git a/pkg/cluster/rbac/ClusterRbacService.go b/pkg/cluster/rbac/ClusterRbacService.go index f4e2f33a1d..c94736b7d1 100644 --- a/pkg/cluster/rbac/ClusterRbacService.go +++ b/pkg/cluster/rbac/ClusterRbacService.go @@ -91,7 +91,7 @@ func (impl *ClusterRbacServiceImpl) CheckAuthorisationForNode(token string, clus func (impl *ClusterRbacServiceImpl) CheckAuthorization(clusterName string, clusterId int, token string, userId int32, rbacForClusterMappingsAlso bool) (authenticated bool, err error) { if rbacForClusterMappingsAlso { - allowedClusterMap, err := impl.FetchAllowedClusterMap(userId) + allowedClusterMap, err := impl.FetchAllowedClusterMap(userId, token) if err != nil { impl.logger.Errorw("error in fetching allowedClusterMap ", "err", err, "clusterName", clusterName) @@ -135,9 +135,9 @@ func (impl *ClusterRbacServiceImpl) CheckAuthorization(clusterName string, clust return false, nil } -func (impl *ClusterRbacServiceImpl) FetchAllowedClusterMap(userId int32) (map[string]bool, error) { +func (impl *ClusterRbacServiceImpl) FetchAllowedClusterMap(userId int32, token string) (map[string]bool, error) { allowedClustersMap := make(map[string]bool) - roles, err := impl.clusterService.FetchRolesFromGroup(userId) + roles, err := impl.clusterService.FetchRolesFromGroup(userId, token) if err != nil { impl.logger.Errorw("error while fetching user roles from db", "error", err) return nil, err diff --git a/pkg/k8s/application/k8sApplicationService.go b/pkg/k8s/application/k8sApplicationService.go index f8188f07f9..550de045bb 100644 --- a/pkg/k8s/application/k8sApplicationService.go +++ b/pkg/k8s/application/k8sApplicationService.go @@ -81,7 +81,7 @@ type K8sApplicationService interface { ValidateClusterResourceBean(ctx context.Context, clusterId int, manifest unstructured.Unstructured, gvk schema.GroupVersionKind, rbacCallback func(clusterName string, resourceIdentifier k8s2.ResourceIdentifier) bool) bool GetResourceInfo(ctx context.Context) (*bean3.ResourceInfo, error) GetAllApiResourceGVKWithoutAuthorization(ctx context.Context, clusterId int) (*k8s2.GetAllApiResourcesResponse, error) - GetAllApiResources(ctx context.Context, clusterId int, isSuperAdmin bool, userId int32) (*k8s2.GetAllApiResourcesResponse, error) + GetAllApiResources(ctx context.Context, clusterId int, isSuperAdmin bool, userId int32, token string) (*k8s2.GetAllApiResourcesResponse, error) GetResourceList(ctx context.Context, token string, request *bean4.ResourceRequestBean, validateResourceAccess func(token string, clusterName string, request bean4.ResourceRequestBean, casbinAction string) bool) (*k8s2.ClusterResourceListMap, error) GetResourceListWithRestConfig(ctx context.Context, token string, request *bean4.ResourceRequestBean, validateResourceAccess func(token string, clusterName string, request bean4.ResourceRequestBean, casbinAction string) bool, restConfig *rest.Config, clusterName string) (*k8s2.ClusterResourceListMap, error) @@ -651,7 +651,7 @@ func (impl *K8sApplicationServiceImpl) GetAllApiResourceGVKWithoutAuthorization( return response, nil } -func (impl *K8sApplicationServiceImpl) GetAllApiResources(ctx context.Context, clusterId int, isSuperAdmin bool, userId int32) (*k8s2.GetAllApiResourcesResponse, error) { +func (impl *K8sApplicationServiceImpl) GetAllApiResources(ctx context.Context, clusterId int, isSuperAdmin bool, userId int32, token string) (*k8s2.GetAllApiResourcesResponse, error) { impl.logger.Infow("getting all api-resources", "clusterId", clusterId) apiResourceGVKResponse, err := impl.GetAllApiResourceGVKWithoutAuthorization(ctx, clusterId) if err != nil { @@ -668,7 +668,7 @@ func (impl *K8sApplicationServiceImpl) GetAllApiResources(ctx context.Context, c impl.logger.Errorw("failed to find cluster for id", "err", err, "clusterId", clusterId) return nil, err } - roles, err := impl.clusterService.FetchRolesFromGroup(userId) + roles, err := impl.clusterService.FetchRolesFromGroup(userId, token) if err != nil { impl.logger.Errorw("error on fetching user roles for cluster list", "err", err) return nil, err diff --git a/pkg/k8s/application/mocks/K8sApplicationService.go b/pkg/k8s/application/mocks/K8sApplicationService.go index 63fd278f5e..3afca4da06 100644 --- a/pkg/k8s/application/mocks/K8sApplicationService.go +++ b/pkg/k8s/application/mocks/K8sApplicationService.go @@ -174,13 +174,13 @@ func (_m *K8sApplicationService) FilterServiceAndIngress(ctx context.Context, re return r0 } -// GetAllApiResources provides a mock function with given fields: ctx, clusterId, isSuperAdmin, userId -func (_m *K8sApplicationService) GetAllApiResources(ctx context.Context, clusterId int, isSuperAdmin bool, userId int32) (*k8s.GetAllApiResourcesResponse, error) { - ret := _m.Called(ctx, clusterId, isSuperAdmin, userId) +// GetAllApiResources provides a mock function with given fields: ctx, clusterId, isSuperAdmin, userId, token +func (_m *K8sApplicationService) GetAllApiResources(ctx context.Context, clusterId int, isSuperAdmin bool, userId int32, token string) (*k8s.GetAllApiResourcesResponse, error) { + ret := _m.Called(ctx, clusterId, isSuperAdmin, userId, token) var r0 *k8s.GetAllApiResourcesResponse - if rf, ok := ret.Get(0).(func(context.Context, int, bool, int32) *k8s.GetAllApiResourcesResponse); ok { - r0 = rf(ctx, clusterId, isSuperAdmin, userId) + if rf, ok := ret.Get(0).(func(context.Context, int, bool, int32, string) *k8s.GetAllApiResourcesResponse); ok { + r0 = rf(ctx, clusterId, isSuperAdmin, userId, token) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*k8s.GetAllApiResourcesResponse) @@ -188,8 +188,8 @@ func (_m *K8sApplicationService) GetAllApiResources(ctx context.Context, cluster } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, int, bool, int32) error); ok { - r1 = rf(ctx, clusterId, isSuperAdmin, userId) + if rf, ok := ret.Get(1).(func(context.Context, int, bool, int32, string) error); ok { + r1 = rf(ctx, clusterId, isSuperAdmin, userId, token) } else { r1 = ret.Error(1) } diff --git a/pkg/userResource/UserResourceService.go b/pkg/userResource/UserResourceService.go index 53fffa9ca6..48274c5fe8 100644 --- a/pkg/userResource/UserResourceService.go +++ b/pkg/userResource/UserResourceService.go @@ -136,7 +136,7 @@ func (impl *UserResourceServiceImpl) getHelmEnvResourceOptions(context context.C func (impl *UserResourceServiceImpl) getClusterResourceOptions(context context.Context, token string, reqBean *apiBean.ResourceOptionsReqDto, params *apiBean.PathParams) (*bean5.ResourceOptionsDto, error) { // System User id is passed as 1 as rbac enforcement is handled globally at bottom level and Super-admin is passed as true here - clusters, err := impl.clusterService.FindAllForClusterByUserId(1, true) + clusters, err := impl.clusterService.FindAllForClusterByUserId(1, true, token) if err != nil { impl.logger.Errorw("error encountered in getClusterResourceOptions", "err", err) return nil, err @@ -147,7 +147,7 @@ func (impl *UserResourceServiceImpl) getClusterResourceOptions(context context.C func (impl *UserResourceServiceImpl) getClusterNamespacesResourceOptions(context context.Context, token string, reqBean *apiBean.ResourceOptionsReqDto, params *apiBean.PathParams) (*bean5.ResourceOptionsDto, error) { // System User id is passed as 1 as rbac enforcement is handled globally at bottom level and Super-admin is passed as true here - namespaces, err := impl.clusterService.FindAllNamespacesByUserIdAndClusterId(bean.SystemUserId, reqBean.ClusterId, true) + namespaces, err := impl.clusterService.FindAllNamespacesByUserIdAndClusterId(bean.SystemUserId, reqBean.ClusterId, true, token) if err != nil { impl.logger.Errorw("error encountered in getClusterNamespacesResourceOptions", "err", err) return nil, err @@ -158,7 +158,7 @@ func (impl *UserResourceServiceImpl) getClusterNamespacesResourceOptions(context func (impl *UserResourceServiceImpl) getClusterApiResourceOptions(context context.Context, token string, reqBean *apiBean.ResourceOptionsReqDto, params *apiBean.PathParams) (*bean5.ResourceOptionsDto, error) { // System User id is passed as 1 as rbac enforcement is handled globally at bottom level and Super-admin is passed as true here - apiResources, err := impl.k8sApplicationService.GetAllApiResources(context, reqBean.ClusterId, true, bean.SystemUserId) + apiResources, err := impl.k8sApplicationService.GetAllApiResources(context, reqBean.ClusterId, true, bean.SystemUserId, token) if err != nil { impl.logger.Errorw("error encountered in getClusterApiResourceOptions", "err", err) return nil, err diff --git a/wire_gen.go b/wire_gen.go index 868b4d10fd..ac743944b0 100644 --- a/wire_gen.go +++ b/wire_gen.go @@ -379,7 +379,7 @@ func InitializeApp() (*App, error) { cronLoggerImpl := cron.NewCronLoggerImpl(sugaredLogger) clusterReadServiceImpl := read2.NewClusterReadServiceImpl(sugaredLogger, clusterRepositoryImpl) runnable := asyncProvider.NewAsyncRunnable(sugaredLogger) - clusterServiceImpl, err := cluster.NewClusterServiceImpl(clusterRepositoryImpl, sugaredLogger, k8sServiceImpl, k8sInformerFactoryImpl, userAuthRepositoryImpl, userRepositoryImpl, roleGroupRepositoryImpl, environmentVariables, cronLoggerImpl, clusterReadServiceImpl, runnable) + clusterServiceImpl, err := cluster.NewClusterServiceImpl(clusterRepositoryImpl, sugaredLogger, k8sServiceImpl, k8sInformerFactoryImpl, userAuthRepositoryImpl, userRepositoryImpl, roleGroupRepositoryImpl, environmentVariables, cronLoggerImpl, clusterReadServiceImpl, runnable, userServiceImpl, globalAuthorisationConfigServiceImpl) if err != nil { return nil, err } From 29cc689ba70546a51cc7075f0a97c6b23b51569b Mon Sep 17 00:00:00 2001 From: mayank-devtron Date: Mon, 16 Mar 2026 13:17:23 +0530 Subject: [PATCH 08/19] feat: update app listing tag filter payload and operators (cherry picked from commit a2866a1b5f54f7a5e6f94d73c6e3011ffabd789d) --- .../app/appList/AppListingRestHandler.go | 7 + .../AppListingRepositoryQueryBuilder.go | 126 +++++++++++++++++- ...RepositoryQueryBuilder_tag_filters_test.go | 75 +++++++++++ pkg/app/AppListingService.go | 65 +++++++-- pkg/app/AppListingService_tag_filter_test.go | 85 ++++++++++++ .../35604400_app_label_filter_index.down.sql | 5 + .../35604400_app_label_filter_index.up.sql | 9 ++ 7 files changed, 356 insertions(+), 16 deletions(-) create mode 100644 internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go create mode 100644 pkg/app/AppListingService_tag_filter_test.go create mode 100644 scripts/sql/35604400_app_label_filter_index.down.sql create mode 100644 scripts/sql/35604400_app_label_filter_index.up.sql diff --git a/api/restHandler/app/appList/AppListingRestHandler.go b/api/restHandler/app/appList/AppListingRestHandler.go index 8c15bee342..8b1249932e 100644 --- a/api/restHandler/app/appList/AppListingRestHandler.go +++ b/api/restHandler/app/appList/AppListingRestHandler.go @@ -331,6 +331,13 @@ func (handler AppListingRestHandlerImpl) FetchAppsByEnvironmentV2(w http.Respons common.WriteJsonResp(w, err, nil, http.StatusBadRequest) return } + normalizedTagFilters, err := app.NormalizeAndValidateTagFilters(fetchAppListingRequest.TagFilters) + if err != nil { + handler.logger.Errorw("request err, ValidateTagFilters", "err", err, "payload", fetchAppListingRequest.TagFilters) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + fetchAppListingRequest.TagFilters = normalizedTagFilters newCtx, span = otel.Tracer("fetchAppListingRequest").Start(newCtx, "GetNamespaceClusterMapping") _, _, err = fetchAppListingRequest.GetNamespaceClusterMapping() span.End() diff --git a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go index b6b879a6ad..c2e4358328 100644 --- a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go +++ b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go @@ -21,6 +21,7 @@ import ( "github.com/devtron-labs/devtron/util" "github.com/go-pg/pg" "go.uber.org/zap" + "strings" ) type AppType int @@ -44,10 +45,11 @@ func NewAppListingRepositoryQueryBuilder(logger *zap.SugaredLogger) AppListingRe } type AppListingFilter struct { - Environments []int `json:"environments"` - Statuses []string `json:"statutes"` - Teams []int `json:"teams"` - AppStatuses []string `json:"appStatuses"` + Environments []int `json:"environments"` + Statuses []string `json:"statutes"` + Teams []int `json:"teams"` + AppStatuses []string `json:"appStatuses"` + TagFilters []TagFilter AppNameSearch string `json:"appNameSearch"` SortOrder SortOrder `json:"sortOrder"` SortBy SortBy `json:"sortBy"` @@ -59,17 +61,53 @@ type AppListingFilter struct { type SortBy string type SortOrder string +type TagFilterOperator string + +// TagFilter holds one row of label filter sent by UI. +// key is always required. +// value is required for EQUALS/DOES_NOT_EQUAL/CONTAINS/DOES_NOT_CONTAIN. +// value must be absent for EXISTS/DOES_NOT_EXIST. +type TagFilter struct { + Key string `json:"key"` + Operator TagFilterOperator `json:"operator"` + Value *string `json:"value"` +} const ( Asc SortOrder = "ASC" Desc SortOrder = "DESC" ) +const ( + TagFilterOperatorEquals TagFilterOperator = "EQUALS" + TagFilterOperatorDoesNotEqual TagFilterOperator = "DOES_NOT_EQUAL" + TagFilterOperatorContains TagFilterOperator = "CONTAINS" + TagFilterOperatorDoesNotContain TagFilterOperator = "DOES_NOT_CONTAIN" + TagFilterOperatorExists TagFilterOperator = "EXISTS" + TagFilterOperatorDoesNotExist TagFilterOperator = "DOES_NOT_EXIST" +) + const ( AppNameSortBy SortBy = "appNameSort" LastDeployedSortBy = "lastDeployedSort" ) +var likePatternEscaper = strings.NewReplacer("\\", "\\\\", "%", "\\%", "_", "\\_") + +func (operator TagFilterOperator) IsValid() bool { + switch operator { + case TagFilterOperatorEquals, + TagFilterOperatorDoesNotEqual, + TagFilterOperatorContains, + TagFilterOperatorDoesNotContain, + TagFilterOperatorExists, + TagFilterOperatorDoesNotExist: + return true + default: + return false + } +} + func (impl AppListingRepositoryQueryBuilder) BuildJobListingQuery(appIDs []int, statuses []string, environmentIds []int, sortOrder string) (string, []interface{}) { var queryParams []interface{} query := `select ci_pipeline.name as ci_pipeline_name,ci_pipeline.id as ci_pipeline_id,app.id as job_id,app.display_name @@ -273,6 +311,17 @@ func (impl AppListingRepositoryQueryBuilder) buildAppListingWhereCondition(appLi whereCondition += " and aps.status IN (?) " queryParams = append(queryParams, pg.In(appStatusExcludingNotDeployed)) } + // Tag filters are AND-combined for now as requested by product. + // Each row translates to a correlated EXISTS/NOT EXISTS on app_label. + tagWhereCondition, tagQueryParams := impl.buildTagFiltersWhereConditionAND(appListingFilter.TagFilters) + whereCondition += tagWhereCondition + queryParams = append(queryParams, tagQueryParams...) + + // Future OR support placeholder (intentionally disabled today): + // orTagWhereCondition, orTagQueryParams := impl.buildTagFiltersWhereConditionOR(appListingFilter.TagFilters) + // whereCondition += orTagWhereCondition + // queryParams = append(queryParams, orTagQueryParams...) + if len(appListingFilter.AppIds) > 0 { whereCondition += " and a.id IN (?) " queryParams = append(queryParams, pg.In(appListingFilter.AppIds)) @@ -280,6 +329,75 @@ func (impl AppListingRepositoryQueryBuilder) buildAppListingWhereCondition(appLi return whereCondition, queryParams } +func (impl AppListingRepositoryQueryBuilder) buildTagFiltersWhereConditionAND(tagFilters []TagFilter) (string, []interface{}) { + if len(tagFilters) == 0 { + return "", nil + } + var queryBuilder strings.Builder + queryParams := make([]interface{}, 0, len(tagFilters)*2) + for _, tagFilter := range tagFilters { + predicate, predicateParams := impl.buildTagFilterPredicate(tagFilter) + queryBuilder.WriteString(" and ") + queryBuilder.WriteString(predicate) + queryParams = append(queryParams, predicateParams...) + } + return queryBuilder.String(), queryParams +} + +// buildTagFiltersWhereConditionOR is intentionally unused today. +// It is kept as documented dead code so switching to OR in future is straightforward. +func (impl AppListingRepositoryQueryBuilder) buildTagFiltersWhereConditionOR(tagFilters []TagFilter) (string, []interface{}) { + if len(tagFilters) == 0 { + return "", nil + } + clauses := make([]string, 0, len(tagFilters)) + queryParams := make([]interface{}, 0, len(tagFilters)*2) + for _, tagFilter := range tagFilters { + predicate, predicateParams := impl.buildTagFilterPredicate(tagFilter) + clauses = append(clauses, predicate) + queryParams = append(queryParams, predicateParams...) + } + return " and (" + strings.Join(clauses, " OR ") + ") ", queryParams +} + +func (impl AppListingRepositoryQueryBuilder) buildTagFilterPredicate(tagFilter TagFilter) (string, []interface{}) { + value := "" + if tagFilter.Value != nil { + value = *tagFilter.Value + } + switch tagFilter.Operator { + case TagFilterOperatorEquals: + return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?)", + []interface{}{tagFilter.Key, value} + case TagFilterOperatorDoesNotEqual: + // NOT EXISTS intentionally includes apps where the key is missing. + return "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?)", + []interface{}{tagFilter.Key, value} + case TagFilterOperatorContains: + return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value LIKE ? ESCAPE '\\')", + []interface{}{tagFilter.Key, buildContainsPattern(value)} + case TagFilterOperatorDoesNotContain: + // NOT EXISTS intentionally includes apps where the key is missing. + return "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value LIKE ? ESCAPE '\\')", + []interface{}{tagFilter.Key, buildContainsPattern(value)} + case TagFilterOperatorExists: + return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ?)", + []interface{}{tagFilter.Key} + case TagFilterOperatorDoesNotExist: + return "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ?)", + []interface{}{tagFilter.Key} + default: + // Invalid operator should never reach here due request validation. + // Returning false condition keeps query safe if validation is bypassed. + return "1 = 0", nil + } +} + +func buildContainsPattern(value string) string { + // Escape SQL LIKE wildcard chars so "contains" behaves like plain substring search. + escaped := likePatternEscaper.Replace(value) + return "%" + escaped + "%" +} func GetCommaSepratedString[T int | string](request []T) string { respString := "" for i, item := range request { diff --git a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go new file mode 100644 index 0000000000..e6de2ffd52 --- /dev/null +++ b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go @@ -0,0 +1,75 @@ +package helper + +import ( + "fmt" + "go.uber.org/zap" + "testing" + + "github.com/stretchr/testify/require" +) + +func stringPointer(value string) *string { + return &value +} + +func TestBuildAppListingWhereCondition_WithTagFiltersAnd(t *testing.T) { + queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) + whereClause, queryParams := queryBuilder.buildAppListingWhereCondition(AppListingFilter{ + TagFilters: []TagFilter{ + {Key: "owner", Operator: TagFilterOperatorEquals, Value: stringPointer("James")}, + {Key: "env", Operator: TagFilterOperatorDoesNotContain, Value: stringPointer("pro_d%")}, + {Key: "team", Operator: TagFilterOperatorExists, Value: nil}, + {Key: "zone", Operator: TagFilterOperatorDoesNotExist, Value: nil}, + }, + }) + + require.Contains(t, whereClause, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?)") + require.Contains(t, whereClause, "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value LIKE ? ESCAPE '\\')") + require.Contains(t, whereClause, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ?)") + require.Contains(t, whereClause, "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ?)") + require.Len(t, queryParams, 8) + require.Equal(t, true, queryParams[0]) + require.Equal(t, CustomApp, queryParams[1]) + require.Equal(t, "owner", queryParams[2]) + require.Equal(t, "James", queryParams[3]) + require.Equal(t, "env", queryParams[4]) + require.Equal(t, "%pro\\_d\\%%", queryParams[5]) + require.Equal(t, "team", queryParams[6]) + require.Equal(t, "zone", queryParams[7]) +} + +func TestBuildTagFiltersWhereConditionOR(t *testing.T) { + queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) + whereClause, queryParams := queryBuilder.buildTagFiltersWhereConditionOR([]TagFilter{ + {Key: "owner", Operator: TagFilterOperatorEquals, Value: stringPointer("James")}, + {Key: "cost-center", Operator: TagFilterOperatorContains, Value: stringPointer("ENG")}, + }) + + require.Equal(t, " and (EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?) OR EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value LIKE ? ESCAPE '\\')) ", whereClause) + require.Equal(t, []interface{}{"owner", "James", "cost-center", "%ENG%"}, queryParams) +} + +func BenchmarkBuildAppListingQueryWithTagFilters(b *testing.B) { + queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) + tagFilters := make([]TagFilter, 0, 10) + for i := 0; i < 10; i++ { + value := fmt.Sprintf("value-%d", i) + tagFilters = append(tagFilters, TagFilter{ + Key: fmt.Sprintf("key-%d", i), + Operator: TagFilterOperatorContains, + Value: &value, + }) + } + appListingFilter := AppListingFilter{ + TagFilters: tagFilters, + SortBy: AppNameSortBy, + SortOrder: Asc, + Offset: 0, + Size: 20, + } + + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = queryBuilder.GetAppIdsQueryWithPaginationForAppNameSearch(appListingFilter) + } +} diff --git a/pkg/app/AppListingService.go b/pkg/app/AppListingService.go index 86f84f2ef3..6a1e80b2f3 100644 --- a/pkg/app/AppListingService.go +++ b/pkg/app/AppListingService.go @@ -82,18 +82,19 @@ const ( ) type FetchAppListingRequest struct { - Environments []int `json:"environments"` - Statuses []string `json:"statuses"` - Teams []int `json:"teams"` - AppNameSearch string `json:"appNameSearch"` - SortOrder helper.SortOrder `json:"sortOrder"` - SortBy helper.SortBy `json:"sortBy"` - Offset int `json:"offset"` - Size int `json:"size"` - DeploymentGroupId int `json:"deploymentGroupId"` - Namespaces []string `json:"namespaces"` // {clusterId}_{namespace} - AppStatuses []string `json:"appStatuses"` - AppIds []int `json:"-"` // internal use only + Environments []int `json:"environments"` + Statuses []string `json:"statuses"` + Teams []int `json:"teams"` + TagFilters []helper.TagFilter `json:"tagFilters"` + AppNameSearch string `json:"appNameSearch"` + SortOrder helper.SortOrder `json:"sortOrder"` + SortBy helper.SortBy `json:"sortBy"` + Offset int `json:"offset"` + Size int `json:"size"` + DeploymentGroupId int `json:"deploymentGroupId"` + Namespaces []string `json:"namespaces"` // {clusterId}_{namespace} + AppStatuses []string `json:"appStatuses"` + AppIds []int `json:"-"` // internal use only // IsClusterOrNamespaceSelected bool `json:"isClusterOrNamespaceSelected"` } type AppNameTypeIdContainer struct { @@ -127,6 +128,40 @@ func (req FetchAppListingRequest) GetNamespaceClusterMapping() (namespaceCluster return namespaceClusterPair, clusterIds, nil } +// NormalizeAndValidateTagFilters validates each tag filter row and normalizes value handling. +// Rules: +// 1) key must be present. +// 2) operator must be one of the supported backend operators. +// 3) for EXISTS/DOES_NOT_EXIST, value must not be sent. +// 4) for EQUALS/DOES_NOT_EQUAL/CONTAINS/DOES_NOT_CONTAIN, value must be non-empty. +func NormalizeAndValidateTagFilters(tagFilters []helper.TagFilter) ([]helper.TagFilter, error) { + if len(tagFilters) == 0 { + return tagFilters, nil + } + normalizedFilters := make([]helper.TagFilter, 0, len(tagFilters)) + for index, tagFilter := range tagFilters { + tagFilter.Key = strings.TrimSpace(tagFilter.Key) + if len(tagFilter.Key) == 0 { + return nil, fmt.Errorf("tagFilters[%d].key is required", index) + } + if !tagFilter.Operator.IsValid() { + return nil, fmt.Errorf("tagFilters[%d].operator is invalid: %s", index, tagFilter.Operator) + } + switch tagFilter.Operator { + case helper.TagFilterOperatorExists, helper.TagFilterOperatorDoesNotExist: + if tagFilter.Value != nil { + return nil, fmt.Errorf("tagFilters[%d].value must be empty for operator %s", index, tagFilter.Operator) + } + default: + if tagFilter.Value == nil || len(*tagFilter.Value) == 0 { + return nil, fmt.Errorf("tagFilters[%d].value is required for operator %s", index, tagFilter.Operator) + } + } + normalizedFilters = append(normalizedFilters, tagFilter) + } + return normalizedFilters, nil +} + type AppListingServiceImpl struct { Logger *zap.SugaredLogger appRepository app.AppRepository @@ -385,6 +420,11 @@ func (impl AppListingServiceImpl) FetchAppsByEnvironmentV2(fetchAppListingReques if len(fetchAppListingRequest.Namespaces) != 0 && len(fetchAppListingRequest.Environments) == 0 { return []*AppView.AppEnvironmentContainer{}, 0, nil } + normalizedTagFilters, err := NormalizeAndValidateTagFilters(fetchAppListingRequest.TagFilters) + if err != nil { + return []*AppView.AppEnvironmentContainer{}, 0, err + } + fetchAppListingRequest.TagFilters = normalizedTagFilters // Currently AppStatus is available in Db for only ArgoApps // We fetch AppStatus on the fly for Helm Apps from scoop, So AppStatus filter will be applied in last @@ -408,6 +448,7 @@ func (impl AppListingServiceImpl) FetchAppsByEnvironmentV2(fetchAppListingReques Size: fetchAppListingRequest.Size, DeploymentGroupId: fetchAppListingRequest.DeploymentGroupId, AppStatuses: fetchAppListingRequest.AppStatuses, + TagFilters: fetchAppListingRequest.TagFilters, AppIds: fetchAppListingRequest.AppIds, } _, span := otel.Tracer("appListingRepository").Start(r.Context(), "FetchAppsByEnvironment") diff --git a/pkg/app/AppListingService_tag_filter_test.go b/pkg/app/AppListingService_tag_filter_test.go new file mode 100644 index 0000000000..824d935e7d --- /dev/null +++ b/pkg/app/AppListingService_tag_filter_test.go @@ -0,0 +1,85 @@ +package app + +import ( + "testing" + + "github.com/devtron-labs/devtron/internal/sql/repository/helper" + "github.com/stretchr/testify/require" +) + +func strPointer(value string) *string { + return &value +} + +func TestNormalizeAndValidateTagFilters_EqualsRequiresValue(t *testing.T) { + _, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ + {Key: "owner", Operator: helper.TagFilterOperatorEquals, Value: nil}, + }) + + require.Error(t, err) + require.Equal(t, "tagFilters[0].value is required for operator EQUALS", err.Error()) +} + +func TestNormalizeAndValidateTagFilters_EqualsRejectsEmptyString(t *testing.T) { + _, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ + {Key: "owner", Operator: helper.TagFilterOperatorEquals, Value: strPointer("")}, + }) + + require.Error(t, err) + require.Equal(t, "tagFilters[0].value is required for operator EQUALS", err.Error()) +} + +func TestNormalizeAndValidateTagFilters_ContainsRequiresValue(t *testing.T) { + _, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ + {Key: "owner", Operator: helper.TagFilterOperatorContains, Value: nil}, + }) + + require.Error(t, err) + require.Equal(t, "tagFilters[0].value is required for operator CONTAINS", err.Error()) +} + +func TestNormalizeAndValidateTagFilters_EmptyKeyReturnsError(t *testing.T) { + _, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ + {Key: " ", Operator: helper.TagFilterOperatorEquals, Value: strPointer("James")}, + }) + + require.Error(t, err) + require.Equal(t, "tagFilters[0].key is required", err.Error()) +} + +func TestNormalizeAndValidateTagFilters_InvalidOperatorReturnsError(t *testing.T) { + _, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ + {Key: "owner", Operator: helper.TagFilterOperator("INVALID"), Value: strPointer("James")}, + }) + + require.Error(t, err) + require.Equal(t, "tagFilters[0].operator is invalid: INVALID", err.Error()) +} + +func TestNormalizeAndValidateTagFilters_ExistsAllowsNilValueOnly(t *testing.T) { + normalizedFilters, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ + {Key: "owner", Operator: helper.TagFilterOperatorExists, Value: nil}, + }) + + require.NoError(t, err) + require.Len(t, normalizedFilters, 1) + require.Nil(t, normalizedFilters[0].Value) +} + +func TestNormalizeAndValidateTagFilters_ExistsRejectsProvidedValue(t *testing.T) { + _, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ + {Key: "owner", Operator: helper.TagFilterOperatorExists, Value: strPointer("James")}, + }) + + require.Error(t, err) + require.Equal(t, "tagFilters[0].value must be empty for operator EXISTS", err.Error()) +} + +func TestNormalizeAndValidateTagFilters_DoesNotExistRejectsProvidedValue(t *testing.T) { + _, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ + {Key: "owner", Operator: helper.TagFilterOperatorDoesNotExist, Value: strPointer("")}, + }) + + require.Error(t, err) + require.Equal(t, "tagFilters[0].value must be empty for operator DOES_NOT_EXIST", err.Error()) +} diff --git a/scripts/sql/35604400_app_label_filter_index.down.sql b/scripts/sql/35604400_app_label_filter_index.down.sql new file mode 100644 index 0000000000..f6be7bb1f3 --- /dev/null +++ b/scripts/sql/35604400_app_label_filter_index.down.sql @@ -0,0 +1,5 @@ +/* + * Copyright (c) 2024. Devtron Inc. + */ + +DROP INDEX IF EXISTS idx_app_label_app_id_key_value; diff --git a/scripts/sql/35604400_app_label_filter_index.up.sql b/scripts/sql/35604400_app_label_filter_index.up.sql new file mode 100644 index 0000000000..a4518f30ce --- /dev/null +++ b/scripts/sql/35604400_app_label_filter_index.up.sql @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024. Devtron Inc. + */ + +-- Index to support app listing tag filters based on app labels. +-- This keeps EXISTS/NOT EXISTS predicates fast for app_id + key lookups, +-- and also helps equality checks on value. +CREATE INDEX IF NOT EXISTS idx_app_label_app_id_key_value + ON public.app_label USING BTREE (app_id, key, value); From f2e20a36f89694daef079bedc462b2129a1d8c1b Mon Sep 17 00:00:00 2001 From: mayank-devtron Date: Mon, 16 Mar 2026 19:20:21 +0530 Subject: [PATCH 09/19] feat: refine tag negative operators behavior (cherry picked from commit 5a5d201c1072b3e147287967ff99666d3fd35122) --- .../AppListingRepositoryQueryBuilder.go | 18 ++++++++--- ...RepositoryQueryBuilder_tag_filters_test.go | 30 ++++++++++++++++++- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go index c2e4358328..eea7ea09dd 100644 --- a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go +++ b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go @@ -360,6 +360,14 @@ func (impl AppListingRepositoryQueryBuilder) buildTagFiltersWhereConditionOR(tag return " and (" + strings.Join(clauses, " OR ") + ") ", queryParams } +// buildTagFilterPredicate converts one UI tag filter row into a SQL predicate. +// Operator behavior (all case-sensitive): +// - EQUALS: key exists with exact value match. +// - DOES_NOT_EQUAL: key exists with at least one value different from target. +// - CONTAINS: key exists with at least one value containing target substring. +// - DOES_NOT_CONTAIN: key exists with at least one value not containing target substring. +// - EXISTS: key exists. +// - DOES_NOT_EXIST: key does not exist. func (impl AppListingRepositoryQueryBuilder) buildTagFilterPredicate(tagFilter TagFilter) (string, []interface{}) { value := "" if tagFilter.Value != nil { @@ -370,15 +378,17 @@ func (impl AppListingRepositoryQueryBuilder) buildTagFilterPredicate(tagFilter T return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?)", []interface{}{tagFilter.Key, value} case TagFilterOperatorDoesNotEqual: - // NOT EXISTS intentionally includes apps where the key is missing. - return "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?)", + // Best-practice semantics for multi-value keys: + // include app when key exists and at least one value is different from target. + return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value <> ?)", []interface{}{tagFilter.Key, value} case TagFilterOperatorContains: return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value LIKE ? ESCAPE '\\')", []interface{}{tagFilter.Key, buildContainsPattern(value)} case TagFilterOperatorDoesNotContain: - // NOT EXISTS intentionally includes apps where the key is missing. - return "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value LIKE ? ESCAPE '\\')", + // Best-practice semantics for multi-value keys: + // include app when key exists and at least one value does not contain target. + return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value NOT LIKE ? ESCAPE '\\')", []interface{}{tagFilter.Key, buildContainsPattern(value)} case TagFilterOperatorExists: return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ?)", diff --git a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go index e6de2ffd52..ceb378c869 100644 --- a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go +++ b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go @@ -24,7 +24,7 @@ func TestBuildAppListingWhereCondition_WithTagFiltersAnd(t *testing.T) { }) require.Contains(t, whereClause, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?)") - require.Contains(t, whereClause, "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value LIKE ? ESCAPE '\\')") + require.Contains(t, whereClause, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value NOT LIKE ? ESCAPE '\\')") require.Contains(t, whereClause, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ?)") require.Contains(t, whereClause, "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ?)") require.Len(t, queryParams, 8) @@ -49,6 +49,34 @@ func TestBuildTagFiltersWhereConditionOR(t *testing.T) { require.Equal(t, []interface{}{"owner", "James", "cost-center", "%ENG%"}, queryParams) } +func TestBuildTagFilterPredicate_DoesNotEqualRequiresKeyAndDifferentValue(t *testing.T) { + queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) + value := "mayank" + + predicate, queryParams := queryBuilder.buildTagFilterPredicate(TagFilter{ + Key: "owner", + Operator: TagFilterOperatorDoesNotEqual, + Value: &value, + }) + + require.Equal(t, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value <> ?)", predicate) + require.Equal(t, []interface{}{"owner", "mayank"}, queryParams) +} + +func TestBuildTagFilterPredicate_DoesNotContainRequiresKeyAndNotLike(t *testing.T) { + queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) + value := "may" + + predicate, queryParams := queryBuilder.buildTagFilterPredicate(TagFilter{ + Key: "owner", + Operator: TagFilterOperatorDoesNotContain, + Value: &value, + }) + + require.Equal(t, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value NOT LIKE ? ESCAPE '\\')", predicate) + require.Equal(t, []interface{}{"owner", "%may%"}, queryParams) +} + func BenchmarkBuildAppListingQueryWithTagFilters(b *testing.B) { queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) tagFilters := make([]TagFilter, 0, 10) From 6b00aa2448944c89f8226c272678a30ff474bc55 Mon Sep 17 00:00:00 2001 From: mayank-devtron Date: Wed, 18 Mar 2026 09:04:17 +0530 Subject: [PATCH 10/19] jsontag (cherry picked from commit 81341fccad519cabd5b94c9b26e4f19b8a832c2c) --- .../sql/repository/helper/AppListingRepositoryQueryBuilder.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go index eea7ea09dd..631c0ddd27 100644 --- a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go +++ b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go @@ -49,7 +49,7 @@ type AppListingFilter struct { Statuses []string `json:"statutes"` Teams []int `json:"teams"` AppStatuses []string `json:"appStatuses"` - TagFilters []TagFilter + TagFilters []TagFilter `json:"tagFilters"` AppNameSearch string `json:"appNameSearch"` SortOrder SortOrder `json:"sortOrder"` SortBy SortBy `json:"sortBy"` From ab2c0e0cd83559e26ff7b05c3c5ae804280f4b28 Mon Sep 17 00:00:00 2001 From: mayank-devtron Date: Wed, 18 Mar 2026 09:36:30 +0530 Subject: [PATCH 11/19] migration renaming (cherry picked from commit c2e625e2ea0aa1feb3d655dd8f28fbf28c96c43a) --- .../AppListingRepositoryQueryBuilder.go | 43 +++++-------------- ...RepositoryQueryBuilder_tag_filters_test.go | 37 ---------------- pkg/app/AppListingService.go | 5 --- ... 35504400_app_label_filter_index.down.sql} | 0 ...=> 35504400_app_label_filter_index.up.sql} | 0 5 files changed, 11 insertions(+), 74 deletions(-) rename scripts/sql/{35604400_app_label_filter_index.down.sql => 35504400_app_label_filter_index.down.sql} (100%) rename scripts/sql/{35604400_app_label_filter_index.up.sql => 35504400_app_label_filter_index.up.sql} (100%) diff --git a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go index 631c0ddd27..0d3cda0db1 100644 --- a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go +++ b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go @@ -45,18 +45,18 @@ func NewAppListingRepositoryQueryBuilder(logger *zap.SugaredLogger) AppListingRe } type AppListingFilter struct { - Environments []int `json:"environments"` - Statuses []string `json:"statutes"` - Teams []int `json:"teams"` - AppStatuses []string `json:"appStatuses"` + Environments []int `json:"environments"` + Statuses []string `json:"statutes"` + Teams []int `json:"teams"` + AppStatuses []string `json:"appStatuses"` TagFilters []TagFilter `json:"tagFilters"` - AppNameSearch string `json:"appNameSearch"` - SortOrder SortOrder `json:"sortOrder"` - SortBy SortBy `json:"sortBy"` - Offset int `json:"offset"` - Size int `json:"size"` - DeploymentGroupId int `json:"deploymentGroupId"` - AppIds []int `json:"-"` // internal use only + AppNameSearch string `json:"appNameSearch"` + SortOrder SortOrder `json:"sortOrder"` + SortBy SortBy `json:"sortBy"` + Offset int `json:"offset"` + Size int `json:"size"` + DeploymentGroupId int `json:"deploymentGroupId"` + AppIds []int `json:"-"` // internal use only } type SortBy string @@ -317,11 +317,6 @@ func (impl AppListingRepositoryQueryBuilder) buildAppListingWhereCondition(appLi whereCondition += tagWhereCondition queryParams = append(queryParams, tagQueryParams...) - // Future OR support placeholder (intentionally disabled today): - // orTagWhereCondition, orTagQueryParams := impl.buildTagFiltersWhereConditionOR(appListingFilter.TagFilters) - // whereCondition += orTagWhereCondition - // queryParams = append(queryParams, orTagQueryParams...) - if len(appListingFilter.AppIds) > 0 { whereCondition += " and a.id IN (?) " queryParams = append(queryParams, pg.In(appListingFilter.AppIds)) @@ -344,22 +339,6 @@ func (impl AppListingRepositoryQueryBuilder) buildTagFiltersWhereConditionAND(ta return queryBuilder.String(), queryParams } -// buildTagFiltersWhereConditionOR is intentionally unused today. -// It is kept as documented dead code so switching to OR in future is straightforward. -func (impl AppListingRepositoryQueryBuilder) buildTagFiltersWhereConditionOR(tagFilters []TagFilter) (string, []interface{}) { - if len(tagFilters) == 0 { - return "", nil - } - clauses := make([]string, 0, len(tagFilters)) - queryParams := make([]interface{}, 0, len(tagFilters)*2) - for _, tagFilter := range tagFilters { - predicate, predicateParams := impl.buildTagFilterPredicate(tagFilter) - clauses = append(clauses, predicate) - queryParams = append(queryParams, predicateParams...) - } - return " and (" + strings.Join(clauses, " OR ") + ") ", queryParams -} - // buildTagFilterPredicate converts one UI tag filter row into a SQL predicate. // Operator behavior (all case-sensitive): // - EQUALS: key exists with exact value match. diff --git a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go index ceb378c869..5fd270b5af 100644 --- a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go +++ b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go @@ -1,7 +1,6 @@ package helper import ( - "fmt" "go.uber.org/zap" "testing" @@ -38,17 +37,6 @@ func TestBuildAppListingWhereCondition_WithTagFiltersAnd(t *testing.T) { require.Equal(t, "zone", queryParams[7]) } -func TestBuildTagFiltersWhereConditionOR(t *testing.T) { - queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) - whereClause, queryParams := queryBuilder.buildTagFiltersWhereConditionOR([]TagFilter{ - {Key: "owner", Operator: TagFilterOperatorEquals, Value: stringPointer("James")}, - {Key: "cost-center", Operator: TagFilterOperatorContains, Value: stringPointer("ENG")}, - }) - - require.Equal(t, " and (EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?) OR EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value LIKE ? ESCAPE '\\')) ", whereClause) - require.Equal(t, []interface{}{"owner", "James", "cost-center", "%ENG%"}, queryParams) -} - func TestBuildTagFilterPredicate_DoesNotEqualRequiresKeyAndDifferentValue(t *testing.T) { queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) value := "mayank" @@ -76,28 +64,3 @@ func TestBuildTagFilterPredicate_DoesNotContainRequiresKeyAndNotLike(t *testing. require.Equal(t, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value NOT LIKE ? ESCAPE '\\')", predicate) require.Equal(t, []interface{}{"owner", "%may%"}, queryParams) } - -func BenchmarkBuildAppListingQueryWithTagFilters(b *testing.B) { - queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) - tagFilters := make([]TagFilter, 0, 10) - for i := 0; i < 10; i++ { - value := fmt.Sprintf("value-%d", i) - tagFilters = append(tagFilters, TagFilter{ - Key: fmt.Sprintf("key-%d", i), - Operator: TagFilterOperatorContains, - Value: &value, - }) - } - appListingFilter := AppListingFilter{ - TagFilters: tagFilters, - SortBy: AppNameSortBy, - SortOrder: Asc, - Offset: 0, - Size: 20, - } - - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _, _ = queryBuilder.GetAppIdsQueryWithPaginationForAppNameSearch(appListingFilter) - } -} diff --git a/pkg/app/AppListingService.go b/pkg/app/AppListingService.go index 6a1e80b2f3..ce5bd32509 100644 --- a/pkg/app/AppListingService.go +++ b/pkg/app/AppListingService.go @@ -420,11 +420,6 @@ func (impl AppListingServiceImpl) FetchAppsByEnvironmentV2(fetchAppListingReques if len(fetchAppListingRequest.Namespaces) != 0 && len(fetchAppListingRequest.Environments) == 0 { return []*AppView.AppEnvironmentContainer{}, 0, nil } - normalizedTagFilters, err := NormalizeAndValidateTagFilters(fetchAppListingRequest.TagFilters) - if err != nil { - return []*AppView.AppEnvironmentContainer{}, 0, err - } - fetchAppListingRequest.TagFilters = normalizedTagFilters // Currently AppStatus is available in Db for only ArgoApps // We fetch AppStatus on the fly for Helm Apps from scoop, So AppStatus filter will be applied in last diff --git a/scripts/sql/35604400_app_label_filter_index.down.sql b/scripts/sql/35504400_app_label_filter_index.down.sql similarity index 100% rename from scripts/sql/35604400_app_label_filter_index.down.sql rename to scripts/sql/35504400_app_label_filter_index.down.sql diff --git a/scripts/sql/35604400_app_label_filter_index.up.sql b/scripts/sql/35504400_app_label_filter_index.up.sql similarity index 100% rename from scripts/sql/35604400_app_label_filter_index.up.sql rename to scripts/sql/35504400_app_label_filter_index.up.sql From c0b293d1e3657fcba6f736915742d9b1c6129722 Mon Sep 17 00:00:00 2001 From: mayank-devtron Date: Wed, 18 Mar 2026 14:08:02 +0530 Subject: [PATCH 12/19] refactor app list tag-filter validation (cherry picked from commit 60ef7488f49db9b56e2f55323066cc8ea8ba2c55) --- .../app/appList/AppListingRestHandler.go | 28 +++++++++-- .../AppListingRepositoryQueryBuilder.go | 4 +- pkg/app/AppListingService.go | 45 ++++++++++++------ pkg/app/AppListingService_tag_filter_test.go | 47 ++++++++++++------- pkg/app/mocks/AppListingService.go | 11 +++++ 5 files changed, 96 insertions(+), 39 deletions(-) diff --git a/api/restHandler/app/appList/AppListingRestHandler.go b/api/restHandler/app/appList/AppListingRestHandler.go index 8b1249932e..a2a800a170 100644 --- a/api/restHandler/app/appList/AppListingRestHandler.go +++ b/api/restHandler/app/appList/AppListingRestHandler.go @@ -56,6 +56,7 @@ import ( "github.com/gorilla/mux" "go.opentelemetry.io/otel" "go.uber.org/zap" + "gopkg.in/go-playground/validator.v9" "net/http" "strconv" "time" @@ -98,6 +99,7 @@ type AppListingRestHandlerImpl struct { k8sApplicationService k8sApplication.K8sApplicationService deploymentConfigService common2.DeploymentConfigService resourceTreeService resourceTree.Service + validator *validator.Validate } type AppStatus struct { @@ -141,6 +143,7 @@ func NewAppListingRestHandlerImpl(appListingService app.AppListingService, k8sApplicationService: k8sApplicationService, deploymentConfigService: deploymentConfigService, resourceTreeService: resourceTreeService, + validator: validator.New(), } return appListingHandler } @@ -276,6 +279,25 @@ func (handler AppListingRestHandlerImpl) FetchJobOverviewCiPipelines(w http.Resp common.WriteJsonResp(w, err, jobCi, http.StatusOK) } +// validateAndNormalizeFetchAppListingRequest applies request-level validation first, +// then tag-filter business validation, and finally normalization. +func (handler AppListingRestHandlerImpl) validateAndNormalizeFetchAppListingRequest(w http.ResponseWriter, r *http.Request, fetchAppListingRequest *app.FetchAppListingRequest) bool { + err := handler.validator.Struct(*fetchAppListingRequest) + if err != nil { + handler.logger.Errorw("validation err, FetchAppsByEnvironment", "err", err, "payload", fetchAppListingRequest) + common.HandleValidationErrors(w, r, err) + return false + } + err = handler.appListingService.ValidateTagFilters(fetchAppListingRequest.TagFilters) + if err != nil { + handler.logger.Errorw("request err, ValidateTagFilters", "err", err, "payload", fetchAppListingRequest.TagFilters) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return false + } + fetchAppListingRequest.TagFilters = handler.appListingService.NormalizeTagFilters(fetchAppListingRequest.TagFilters) + return true +} + func (handler AppListingRestHandlerImpl) FetchAppsByEnvironmentV2(w http.ResponseWriter, r *http.Request) { //Allow CORS here By * or specific origin util3.SetupCorsOriginHeader(&w) @@ -331,13 +353,9 @@ func (handler AppListingRestHandlerImpl) FetchAppsByEnvironmentV2(w http.Respons common.WriteJsonResp(w, err, nil, http.StatusBadRequest) return } - normalizedTagFilters, err := app.NormalizeAndValidateTagFilters(fetchAppListingRequest.TagFilters) - if err != nil { - handler.logger.Errorw("request err, ValidateTagFilters", "err", err, "payload", fetchAppListingRequest.TagFilters) - common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + if !handler.validateAndNormalizeFetchAppListingRequest(w, r, &fetchAppListingRequest) { return } - fetchAppListingRequest.TagFilters = normalizedTagFilters newCtx, span = otel.Tracer("fetchAppListingRequest").Start(newCtx, "GetNamespaceClusterMapping") _, _, err = fetchAppListingRequest.GetNamespaceClusterMapping() span.End() diff --git a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go index 0d3cda0db1..ed640798b3 100644 --- a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go +++ b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go @@ -68,8 +68,8 @@ type TagFilterOperator string // value is required for EQUALS/DOES_NOT_EQUAL/CONTAINS/DOES_NOT_CONTAIN. // value must be absent for EXISTS/DOES_NOT_EXIST. type TagFilter struct { - Key string `json:"key"` - Operator TagFilterOperator `json:"operator"` + Key string `json:"key" validate:"required"` + Operator TagFilterOperator `json:"operator" validate:"required"` Value *string `json:"value"` } diff --git a/pkg/app/AppListingService.go b/pkg/app/AppListingService.go index ce5bd32509..c21aa5f1d1 100644 --- a/pkg/app/AppListingService.go +++ b/pkg/app/AppListingService.go @@ -70,6 +70,8 @@ type AppListingService interface { ISLastReleaseStopType(appId, envId int) (bool, error) ISLastReleaseStopTypeV2(pipelineIds []int) (map[int]bool, error) GetReleaseCount(appId, envId int) (int, error) + ValidateTagFilters(tagFilters []helper.TagFilter) error + NormalizeTagFilters(tagFilters []helper.TagFilter) []helper.TagFilter FetchAppsByEnvironmentV2(fetchAppListingRequest FetchAppListingRequest, w http.ResponseWriter, r *http.Request, token string) ([]*AppView.AppEnvironmentContainer, int, error) FetchOverviewAppsByEnvironment(envId, limit, offset int) (*OverviewAppsByEnvironmentBean, error) @@ -85,7 +87,7 @@ type FetchAppListingRequest struct { Environments []int `json:"environments"` Statuses []string `json:"statuses"` Teams []int `json:"teams"` - TagFilters []helper.TagFilter `json:"tagFilters"` + TagFilters []helper.TagFilter `json:"tagFilters" validate:"omitempty,dive"` AppNameSearch string `json:"appNameSearch"` SortOrder helper.SortOrder `json:"sortOrder"` SortBy helper.SortBy `json:"sortBy"` @@ -128,38 +130,53 @@ func (req FetchAppListingRequest) GetNamespaceClusterMapping() (namespaceCluster return namespaceClusterPair, clusterIds, nil } -// NormalizeAndValidateTagFilters validates each tag filter row and normalizes value handling. +// ValidateTagFilters validates each tag filter row. // Rules: // 1) key must be present. // 2) operator must be one of the supported backend operators. // 3) for EXISTS/DOES_NOT_EXIST, value must not be sent. // 4) for EQUALS/DOES_NOT_EQUAL/CONTAINS/DOES_NOT_CONTAIN, value must be non-empty. -func NormalizeAndValidateTagFilters(tagFilters []helper.TagFilter) ([]helper.TagFilter, error) { - if len(tagFilters) == 0 { - return tagFilters, nil - } - normalizedFilters := make([]helper.TagFilter, 0, len(tagFilters)) +func ValidateTagFilters(tagFilters []helper.TagFilter) error { for index, tagFilter := range tagFilters { - tagFilter.Key = strings.TrimSpace(tagFilter.Key) - if len(tagFilter.Key) == 0 { - return nil, fmt.Errorf("tagFilters[%d].key is required", index) + if len(strings.TrimSpace(tagFilter.Key)) == 0 { + return fmt.Errorf("tagFilters[%d].key is required", index) } if !tagFilter.Operator.IsValid() { - return nil, fmt.Errorf("tagFilters[%d].operator is invalid: %s", index, tagFilter.Operator) + return fmt.Errorf("tagFilters[%d].operator is invalid: %s", index, tagFilter.Operator) } switch tagFilter.Operator { case helper.TagFilterOperatorExists, helper.TagFilterOperatorDoesNotExist: if tagFilter.Value != nil { - return nil, fmt.Errorf("tagFilters[%d].value must be empty for operator %s", index, tagFilter.Operator) + return fmt.Errorf("tagFilters[%d].value must be empty for operator %s", index, tagFilter.Operator) } default: if tagFilter.Value == nil || len(*tagFilter.Value) == 0 { - return nil, fmt.Errorf("tagFilters[%d].value is required for operator %s", index, tagFilter.Operator) + return fmt.Errorf("tagFilters[%d].value is required for operator %s", index, tagFilter.Operator) } } + } + return nil +} + +// NormalizeTagFilters normalizes tag filter rows after validation. +func NormalizeTagFilters(tagFilters []helper.TagFilter) []helper.TagFilter { + if len(tagFilters) == 0 { + return tagFilters + } + normalizedFilters := make([]helper.TagFilter, 0, len(tagFilters)) + for _, tagFilter := range tagFilters { + tagFilter.Key = strings.TrimSpace(tagFilter.Key) normalizedFilters = append(normalizedFilters, tagFilter) } - return normalizedFilters, nil + return normalizedFilters +} + +func (impl AppListingServiceImpl) ValidateTagFilters(tagFilters []helper.TagFilter) error { + return ValidateTagFilters(tagFilters) +} + +func (impl AppListingServiceImpl) NormalizeTagFilters(tagFilters []helper.TagFilter) []helper.TagFilter { + return NormalizeTagFilters(tagFilters) } type AppListingServiceImpl struct { diff --git a/pkg/app/AppListingService_tag_filter_test.go b/pkg/app/AppListingService_tag_filter_test.go index 824d935e7d..3c4a0dfdd5 100644 --- a/pkg/app/AppListingService_tag_filter_test.go +++ b/pkg/app/AppListingService_tag_filter_test.go @@ -11,8 +11,8 @@ func strPointer(value string) *string { return &value } -func TestNormalizeAndValidateTagFilters_EqualsRequiresValue(t *testing.T) { - _, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ +func TestValidateTagFilters_EqualsRequiresValue(t *testing.T) { + err := ValidateTagFilters([]helper.TagFilter{ {Key: "owner", Operator: helper.TagFilterOperatorEquals, Value: nil}, }) @@ -20,8 +20,8 @@ func TestNormalizeAndValidateTagFilters_EqualsRequiresValue(t *testing.T) { require.Equal(t, "tagFilters[0].value is required for operator EQUALS", err.Error()) } -func TestNormalizeAndValidateTagFilters_EqualsRejectsEmptyString(t *testing.T) { - _, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ +func TestValidateTagFilters_EqualsRejectsEmptyString(t *testing.T) { + err := ValidateTagFilters([]helper.TagFilter{ {Key: "owner", Operator: helper.TagFilterOperatorEquals, Value: strPointer("")}, }) @@ -29,8 +29,8 @@ func TestNormalizeAndValidateTagFilters_EqualsRejectsEmptyString(t *testing.T) { require.Equal(t, "tagFilters[0].value is required for operator EQUALS", err.Error()) } -func TestNormalizeAndValidateTagFilters_ContainsRequiresValue(t *testing.T) { - _, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ +func TestValidateTagFilters_ContainsRequiresValue(t *testing.T) { + err := ValidateTagFilters([]helper.TagFilter{ {Key: "owner", Operator: helper.TagFilterOperatorContains, Value: nil}, }) @@ -38,8 +38,8 @@ func TestNormalizeAndValidateTagFilters_ContainsRequiresValue(t *testing.T) { require.Equal(t, "tagFilters[0].value is required for operator CONTAINS", err.Error()) } -func TestNormalizeAndValidateTagFilters_EmptyKeyReturnsError(t *testing.T) { - _, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ +func TestValidateTagFilters_EmptyKeyReturnsError(t *testing.T) { + err := ValidateTagFilters([]helper.TagFilter{ {Key: " ", Operator: helper.TagFilterOperatorEquals, Value: strPointer("James")}, }) @@ -47,8 +47,8 @@ func TestNormalizeAndValidateTagFilters_EmptyKeyReturnsError(t *testing.T) { require.Equal(t, "tagFilters[0].key is required", err.Error()) } -func TestNormalizeAndValidateTagFilters_InvalidOperatorReturnsError(t *testing.T) { - _, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ +func TestValidateTagFilters_InvalidOperatorReturnsError(t *testing.T) { + err := ValidateTagFilters([]helper.TagFilter{ {Key: "owner", Operator: helper.TagFilterOperator("INVALID"), Value: strPointer("James")}, }) @@ -56,18 +56,16 @@ func TestNormalizeAndValidateTagFilters_InvalidOperatorReturnsError(t *testing.T require.Equal(t, "tagFilters[0].operator is invalid: INVALID", err.Error()) } -func TestNormalizeAndValidateTagFilters_ExistsAllowsNilValueOnly(t *testing.T) { - normalizedFilters, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ +func TestValidateTagFilters_ExistsAllowsNilValueOnly(t *testing.T) { + err := ValidateTagFilters([]helper.TagFilter{ {Key: "owner", Operator: helper.TagFilterOperatorExists, Value: nil}, }) require.NoError(t, err) - require.Len(t, normalizedFilters, 1) - require.Nil(t, normalizedFilters[0].Value) } -func TestNormalizeAndValidateTagFilters_ExistsRejectsProvidedValue(t *testing.T) { - _, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ +func TestValidateTagFilters_ExistsRejectsProvidedValue(t *testing.T) { + err := ValidateTagFilters([]helper.TagFilter{ {Key: "owner", Operator: helper.TagFilterOperatorExists, Value: strPointer("James")}, }) @@ -75,11 +73,24 @@ func TestNormalizeAndValidateTagFilters_ExistsRejectsProvidedValue(t *testing.T) require.Equal(t, "tagFilters[0].value must be empty for operator EXISTS", err.Error()) } -func TestNormalizeAndValidateTagFilters_DoesNotExistRejectsProvidedValue(t *testing.T) { - _, err := NormalizeAndValidateTagFilters([]helper.TagFilter{ +func TestValidateTagFilters_DoesNotExistRejectsProvidedValue(t *testing.T) { + err := ValidateTagFilters([]helper.TagFilter{ {Key: "owner", Operator: helper.TagFilterOperatorDoesNotExist, Value: strPointer("")}, }) require.Error(t, err) require.Equal(t, "tagFilters[0].value must be empty for operator DOES_NOT_EXIST", err.Error()) } + +func TestNormalizeTagFilters_TrimsKey(t *testing.T) { + filters := []helper.TagFilter{ + {Key: " owner ", Operator: helper.TagFilterOperatorEquals, Value: strPointer("James")}, + } + + normalizedFilters := NormalizeTagFilters(filters) + + require.Len(t, normalizedFilters, 1) + require.Equal(t, "owner", normalizedFilters[0].Key) + // Ensure input is not modified by normalization. + require.Equal(t, " owner ", filters[0].Key) +} diff --git a/pkg/app/mocks/AppListingService.go b/pkg/app/mocks/AppListingService.go index b321294fa1..6cab7df9c9 100644 --- a/pkg/app/mocks/AppListingService.go +++ b/pkg/app/mocks/AppListingService.go @@ -4,6 +4,7 @@ package mocks import ( bean "github.com/devtron-labs/devtron/api/bean/AppView" + helper "github.com/devtron-labs/devtron/internal/sql/repository/helper" app "github.com/devtron-labs/devtron/pkg/app" context "context" @@ -313,6 +314,16 @@ func (_m *AppListingService) FetchAppsByEnvironmentV2(fetchAppListingRequest app return r0, r1, r2 } +// NormalizeTagFilters provides a mock function with given fields: tagFilters +func (_m *AppListingService) NormalizeTagFilters(tagFilters []helper.TagFilter) []helper.TagFilter { + return app.NormalizeTagFilters(tagFilters) +} + +// ValidateTagFilters provides a mock function with given fields: tagFilters +func (_m *AppListingService) ValidateTagFilters(tagFilters []helper.TagFilter) error { + return app.ValidateTagFilters(tagFilters) +} + // FetchJobs provides a mock function with given fields: fetchJobListingRequest func (_m *AppListingService) FetchJobs(fetchJobListingRequest app.FetchAppListingRequest) ([]*bean.JobContainer, error) { ret := _m.Called(fetchJobListingRequest) From c13898f48d2ebb6a7d490bd8e5841834e72f9f04 Mon Sep 17 00:00:00 2001 From: mayank-devtron Date: Fri, 20 Mar 2026 13:06:13 +0530 Subject: [PATCH 13/19] feat: add tag filter support in app listing (cherry picked from commit 57e4fd6b65185e2f3f6add7d7010f4c1ebdcf1fc) --- .../app/appList/AppListingRestHandler.go | 14 ++- .../sql/repository/AppListingRepository.go | 28 ++++- .../AppListingRepositoryQueryBuilder.go | 118 +++++++++++------- ...RepositoryQueryBuilder_tag_filters_test.go | 70 +++++++++-- pkg/app/AppListingService.go | 4 +- 5 files changed, 165 insertions(+), 69 deletions(-) diff --git a/api/restHandler/app/appList/AppListingRestHandler.go b/api/restHandler/app/appList/AppListingRestHandler.go index a2a800a170..3b55b9c2a4 100644 --- a/api/restHandler/app/appList/AppListingRestHandler.go +++ b/api/restHandler/app/appList/AppListingRestHandler.go @@ -279,9 +279,8 @@ func (handler AppListingRestHandlerImpl) FetchJobOverviewCiPipelines(w http.Resp common.WriteJsonResp(w, err, jobCi, http.StatusOK) } -// validateAndNormalizeFetchAppListingRequest applies request-level validation first, -// then tag-filter business validation, and finally normalization. -func (handler AppListingRestHandlerImpl) validateAndNormalizeFetchAppListingRequest(w http.ResponseWriter, r *http.Request, fetchAppListingRequest *app.FetchAppListingRequest) bool { +// validateFetchAppListingRequest performs request and business-rule validation. +func (handler AppListingRestHandlerImpl) validateFetchAppListingRequest(w http.ResponseWriter, r *http.Request, fetchAppListingRequest *app.FetchAppListingRequest) bool { err := handler.validator.Struct(*fetchAppListingRequest) if err != nil { handler.logger.Errorw("validation err, FetchAppsByEnvironment", "err", err, "payload", fetchAppListingRequest) @@ -294,10 +293,14 @@ func (handler AppListingRestHandlerImpl) validateAndNormalizeFetchAppListingRequ common.WriteJsonResp(w, err, nil, http.StatusBadRequest) return false } - fetchAppListingRequest.TagFilters = handler.appListingService.NormalizeTagFilters(fetchAppListingRequest.TagFilters) return true } +// normalizeFetchAppListingRequest applies request normalization after validation. +func (handler AppListingRestHandlerImpl) normalizeFetchAppListingRequest(fetchAppListingRequest *app.FetchAppListingRequest) { + fetchAppListingRequest.TagFilters = handler.appListingService.NormalizeTagFilters(fetchAppListingRequest.TagFilters) +} + func (handler AppListingRestHandlerImpl) FetchAppsByEnvironmentV2(w http.ResponseWriter, r *http.Request) { //Allow CORS here By * or specific origin util3.SetupCorsOriginHeader(&w) @@ -353,9 +356,10 @@ func (handler AppListingRestHandlerImpl) FetchAppsByEnvironmentV2(w http.Respons common.WriteJsonResp(w, err, nil, http.StatusBadRequest) return } - if !handler.validateAndNormalizeFetchAppListingRequest(w, r, &fetchAppListingRequest) { + if !handler.validateFetchAppListingRequest(w, r, &fetchAppListingRequest) { return } + handler.normalizeFetchAppListingRequest(&fetchAppListingRequest) newCtx, span = otel.Tracer("fetchAppListingRequest").Start(newCtx, "GetNamespaceClusterMapping") _, _, err = fetchAppListingRequest.GetNamespaceClusterMapping() span.End() diff --git a/internal/sql/repository/AppListingRepository.go b/internal/sql/repository/AppListingRepository.go index 7c72caac99..077a4994bf 100644 --- a/internal/sql/repository/AppListingRepository.go +++ b/internal/sql/repository/AppListingRepository.go @@ -217,10 +217,14 @@ func (impl *AppListingRepositoryImpl) FetchAppsByEnvironmentV2(appListingFilter if string(appListingFilter.SortBy) == helper.LastDeployedSortBy { - query, queryParams := impl.appListingRepositoryQueryBuilder.GetAppIdsQueryWithPaginationForLastDeployedSearch(appListingFilter) + query, queryParams, err := impl.appListingRepositoryQueryBuilder.GetAppIdsQueryWithPaginationForLastDeployedSearch(appListingFilter) + if err != nil { + impl.Logger.Errorw("error in building appIds query with appList filter", "err", err, "filter", appListingFilter) + return appEnvArr, appsSize, err + } impl.Logger.Debug("GetAppIdsQueryWithPaginationForLastDeployedSearch query ", query) start := time.Now() - _, err := impl.dbConnection.Query(&lastDeployedTimeDTO, query, queryParams...) + _, err = impl.dbConnection.Query(&lastDeployedTimeDTO, query, queryParams...) middleware.AppListingDuration.WithLabelValues("getAppIdsQueryWithPaginationForLastDeployedSearch", "devtron").Observe(time.Since(start).Seconds()) if err != nil || len(lastDeployedTimeDTO) == 0 { if err != nil { @@ -235,7 +239,11 @@ func (impl *AppListingRepositoryImpl) FetchAppsByEnvironmentV2(appListingFilter appIdsFound[i] = obj.AppId } appListingFilter.AppIds = appIdsFound - appContainerQuery, appContainerQueryParams := impl.appListingRepositoryQueryBuilder.GetQueryForAppEnvContainers(appListingFilter) + appContainerQuery, appContainerQueryParams, err := impl.appListingRepositoryQueryBuilder.GetQueryForAppEnvContainers(appListingFilter) + if err != nil { + impl.Logger.Errorw("error in building appEnv query with appList filter", "err", err, "filter", appListingFilter) + return appEnvArr, appsSize, err + } impl.Logger.Debug("GetQueryForAppEnvContainers query ", query) _, err = impl.dbConnection.Query(&appEnvContainer, appContainerQuery, appContainerQueryParams...) if err != nil { @@ -247,10 +255,14 @@ func (impl *AppListingRepositoryImpl) FetchAppsByEnvironmentV2(appListingFilter // to get all the appIds in appEnvs allowed for user and filtered by the appListing filter and sorted by name appIdCountDtos := make([]*AppView.AppEnvironmentContainer, 0) - appIdCountQuery, appIdCountQueryParams := impl.appListingRepositoryQueryBuilder.GetAppIdsQueryWithPaginationForAppNameSearch(appListingFilter) + appIdCountQuery, appIdCountQueryParams, appsErr := impl.appListingRepositoryQueryBuilder.GetAppIdsQueryWithPaginationForAppNameSearch(appListingFilter) + if appsErr != nil { + impl.Logger.Errorw("error in building appIds query with appList filter", "err", appsErr, "filter", appListingFilter) + return appEnvContainer, appsSize, appsErr + } impl.Logger.Debug("GetAppIdsQueryWithPaginationForAppNameSearch query ", appIdCountQuery) start := time.Now() - _, appsErr := impl.dbConnection.Query(&appIdCountDtos, appIdCountQuery, appIdCountQueryParams...) + _, appsErr = impl.dbConnection.Query(&appIdCountDtos, appIdCountQuery, appIdCountQueryParams...) middleware.AppListingDuration.WithLabelValues("getAppIdsQueryWithPaginationForAppNameSearch", "devtron").Observe(time.Since(start).Seconds()) if appsErr != nil || len(appIdCountDtos) == 0 { if appsErr != nil { @@ -268,7 +280,11 @@ func (impl *AppListingRepositoryImpl) FetchAppsByEnvironmentV2(appListingFilter appListingFilter.AppIds = uniqueAppIds // set appids required for this page in the filter and get the appEnv containers of these apps appListingFilter.AppIds = uniqueAppIds - appsEnvquery, appsEnvQueryParams := impl.appListingRepositoryQueryBuilder.GetQueryForAppEnvContainers(appListingFilter) + appsEnvquery, appsEnvQueryParams, appsErr := impl.appListingRepositoryQueryBuilder.GetQueryForAppEnvContainers(appListingFilter) + if appsErr != nil { + impl.Logger.Errorw("error in building appEnv query with appList filter", "err", appsErr, "filter", appListingFilter) + return appEnvContainer, appsSize, appsErr + } impl.Logger.Debug("GetQueryForAppEnvContainers query: ", appsEnvquery) start = time.Now() _, appsErr = impl.dbConnection.Query(&appEnvContainer, appsEnvquery, appsEnvQueryParams...) diff --git a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go index ed640798b3..f9ed6416f3 100644 --- a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go +++ b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go @@ -45,18 +45,18 @@ func NewAppListingRepositoryQueryBuilder(logger *zap.SugaredLogger) AppListingRe } type AppListingFilter struct { - Environments []int `json:"environments"` - Statuses []string `json:"statutes"` - Teams []int `json:"teams"` - AppStatuses []string `json:"appStatuses"` - TagFilters []TagFilter `json:"tagFilters"` - AppNameSearch string `json:"appNameSearch"` - SortOrder SortOrder `json:"sortOrder"` - SortBy SortBy `json:"sortBy"` - Offset int `json:"offset"` - Size int `json:"size"` - DeploymentGroupId int `json:"deploymentGroupId"` - AppIds []int `json:"-"` // internal use only + Environments []int `json:"environments"` + Statuses []string `json:"statutes"` + Teams []int `json:"teams"` + AppStatuses []string `json:"appStatuses"` + TagFilters *[]TagFilter `json:"tagFilters"` + AppNameSearch string `json:"appNameSearch"` + SortOrder SortOrder `json:"sortOrder"` + SortBy SortBy `json:"sortBy"` + Offset int `json:"offset"` + Size int `json:"size"` + DeploymentGroupId int `json:"deploymentGroupId"` + AppIds []int `json:"-"` // internal use only } type SortBy string @@ -170,14 +170,17 @@ func getAppListingCommonQueryString() string { " LEFT JOIN app_status aps on aps.app_id = a.id and p.environment_id = aps.env_id " } -func (impl AppListingRepositoryQueryBuilder) GetQueryForAppEnvContainers(appListingFilter AppListingFilter) (string, []interface{}) { +func (impl AppListingRepositoryQueryBuilder) GetQueryForAppEnvContainers(appListingFilter AppListingFilter) (string, []interface{}, error) { query := "SELECT p.environment_id , a.id AS app_id, a.app_name,p.id as pipeline_id, a.team_id ,aps.status as app_status " - queryTemp, queryParams := impl.TestForCommonAppFilter(appListingFilter) + queryTemp, queryParams, err := impl.TestForCommonAppFilter(appListingFilter) + if err != nil { + return "", nil, err + } query += queryTemp - return query, queryParams + return query, queryParams, nil } -func (impl AppListingRepositoryQueryBuilder) CommonJoinSubQuery(appListingFilter AppListingFilter) (string, []interface{}) { +func (impl AppListingRepositoryQueryBuilder) CommonJoinSubQuery(appListingFilter AppListingFilter) (string, []interface{}, error) { var queryParams []interface{} query := ` LEFT JOIN pipeline p ON a.id=p.app_id and p.deleted=? LEFT JOIN deployment_config dc ON ( p.app_id=dc.app_id and p.environment_id=dc.environment_id and dc.active=? ) @@ -186,16 +189,22 @@ func (impl AppListingRepositoryQueryBuilder) CommonJoinSubQuery(appListingFilter if appListingFilter.DeploymentGroupId != 0 { query = query + " INNER JOIN deployment_group_app dga ON a.id = dga.app_id " } - whereCondition, whereConditionParams := impl.buildAppListingWhereCondition(appListingFilter) + whereCondition, whereConditionParams, err := impl.buildAppListingWhereCondition(appListingFilter) + if err != nil { + return "", nil, err + } query = query + whereCondition queryParams = append(queryParams, whereConditionParams...) - return query, queryParams + return query, queryParams, nil } -func (impl AppListingRepositoryQueryBuilder) TestForCommonAppFilter(appListingFilter AppListingFilter) (string, []interface{}) { - queryTemp, queryParams := impl.CommonJoinSubQuery(appListingFilter) +func (impl AppListingRepositoryQueryBuilder) TestForCommonAppFilter(appListingFilter AppListingFilter) (string, []interface{}, error) { + queryTemp, queryParams, err := impl.CommonJoinSubQuery(appListingFilter) + if err != nil { + return "", nil, err + } query := " FROM app a " + queryTemp - return query, queryParams + return query, queryParams, nil } func (impl AppListingRepositoryQueryBuilder) BuildAppListingQueryLastDeploymentTimeV2(pipelineIDs []int) (string, []interface{}) { @@ -211,8 +220,11 @@ func (impl AppListingRepositoryQueryBuilder) BuildAppListingQueryLastDeploymentT return query, queryParams } -func (impl AppListingRepositoryQueryBuilder) GetAppIdsQueryWithPaginationForLastDeployedSearch(appListingFilter AppListingFilter) (string, []interface{}) { - join, queryParams := impl.CommonJoinSubQuery(appListingFilter) +func (impl AppListingRepositoryQueryBuilder) GetAppIdsQueryWithPaginationForLastDeployedSearch(appListingFilter AppListingFilter) (string, []interface{}, error) { + join, queryParams, err := impl.CommonJoinSubQuery(appListingFilter) + if err != nil { + return "", nil, err + } countQuery := " (SELECT count(distinct(a.id)) as count FROM app a " + join + ") AS total_count " // appending query params for count query as well queryParams = append(queryParams, queryParams...) @@ -234,12 +246,15 @@ func (impl AppListingRepositoryQueryBuilder) GetAppIdsQueryWithPaginationForLast } query += " LIMIT ? OFFSET ? " queryParams = append(queryParams, appListingFilter.Size, appListingFilter.Offset) - return query, queryParams + return query, queryParams, nil } -func (impl AppListingRepositoryQueryBuilder) GetAppIdsQueryWithPaginationForAppNameSearch(appListingFilter AppListingFilter) (string, []interface{}) { +func (impl AppListingRepositoryQueryBuilder) GetAppIdsQueryWithPaginationForAppNameSearch(appListingFilter AppListingFilter) (string, []interface{}, error) { orderByClause := impl.buildAppListingSortBy(appListingFilter) - join, queryParams := impl.CommonJoinSubQuery(appListingFilter) + join, queryParams, err := impl.CommonJoinSubQuery(appListingFilter) + if err != nil { + return "", nil, err + } countQuery := "( SELECT count(distinct(a.id)) as count FROM app a" + join + " ) as total_count" query := "SELECT DISTINCT(a.id) as app_id, a.app_name, " + countQuery + " FROM app a " + join @@ -250,7 +265,7 @@ func (impl AppListingRepositoryQueryBuilder) GetAppIdsQueryWithPaginationForAppN //adding queryParams two times because join query is used in countQuery and mainQuery two times queryParams = append(queryParams, queryParams...) queryParams = append(queryParams, appListingFilter.Size, appListingFilter.Offset) - return query, queryParams + return query, queryParams, nil } func (impl AppListingRepositoryQueryBuilder) buildAppListingSortBy(appListingFilter AppListingFilter) string { @@ -263,7 +278,7 @@ func (impl AppListingRepositoryQueryBuilder) buildAppListingSortBy(appListingFil return orderByCondition } -func (impl AppListingRepositoryQueryBuilder) buildAppListingWhereCondition(appListingFilter AppListingFilter) (string, []interface{}) { +func (impl AppListingRepositoryQueryBuilder) buildAppListingWhereCondition(appListingFilter AppListingFilter) (string, []interface{}, error) { var queryParams []interface{} whereCondition := " WHERE a.active = ? and a.app_type = ? " queryParams = append(queryParams, true, CustomApp) @@ -313,30 +328,39 @@ func (impl AppListingRepositoryQueryBuilder) buildAppListingWhereCondition(appLi } // Tag filters are AND-combined for now as requested by product. // Each row translates to a correlated EXISTS/NOT EXISTS on app_label. - tagWhereCondition, tagQueryParams := impl.buildTagFiltersWhereConditionAND(appListingFilter.TagFilters) + tagWhereCondition, tagQueryParams, err := impl.buildTagFiltersWhereConditionAND(appListingFilter.TagFilters) + if err != nil { + return "", nil, err + } whereCondition += tagWhereCondition - queryParams = append(queryParams, tagQueryParams...) + if len(tagQueryParams) > 0 { + queryParams = append(queryParams, tagQueryParams...) + } if len(appListingFilter.AppIds) > 0 { whereCondition += " and a.id IN (?) " queryParams = append(queryParams, pg.In(appListingFilter.AppIds)) } - return whereCondition, queryParams + + return whereCondition, queryParams, nil } -func (impl AppListingRepositoryQueryBuilder) buildTagFiltersWhereConditionAND(tagFilters []TagFilter) (string, []interface{}) { - if len(tagFilters) == 0 { - return "", nil +func (impl AppListingRepositoryQueryBuilder) buildTagFiltersWhereConditionAND(tagFilters *[]TagFilter) (string, []interface{}, error) { + if tagFilters == nil || len(*tagFilters) == 0 { + return "", make([]interface{}, 0), nil } var queryBuilder strings.Builder - queryParams := make([]interface{}, 0, len(tagFilters)*2) - for _, tagFilter := range tagFilters { - predicate, predicateParams := impl.buildTagFilterPredicate(tagFilter) + queryParams := make([]interface{}, 0, len(*tagFilters)*2) + for _, tagFilter := range *tagFilters { + predicate, predicateParams, err := impl.buildTagFilterPredicate(tagFilter) + if err != nil { + return "", nil, err + } queryBuilder.WriteString(" and ") queryBuilder.WriteString(predicate) queryParams = append(queryParams, predicateParams...) } - return queryBuilder.String(), queryParams + return queryBuilder.String(), queryParams, nil } // buildTagFilterPredicate converts one UI tag filter row into a SQL predicate. @@ -347,7 +371,7 @@ func (impl AppListingRepositoryQueryBuilder) buildTagFiltersWhereConditionAND(ta // - DOES_NOT_CONTAIN: key exists with at least one value not containing target substring. // - EXISTS: key exists. // - DOES_NOT_EXIST: key does not exist. -func (impl AppListingRepositoryQueryBuilder) buildTagFilterPredicate(tagFilter TagFilter) (string, []interface{}) { +func (impl AppListingRepositoryQueryBuilder) buildTagFilterPredicate(tagFilter TagFilter) (string, []interface{}, error) { value := "" if tagFilter.Value != nil { value = *tagFilter.Value @@ -355,30 +379,28 @@ func (impl AppListingRepositoryQueryBuilder) buildTagFilterPredicate(tagFilter T switch tagFilter.Operator { case TagFilterOperatorEquals: return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?)", - []interface{}{tagFilter.Key, value} + []interface{}{tagFilter.Key, value}, nil case TagFilterOperatorDoesNotEqual: // Best-practice semantics for multi-value keys: // include app when key exists and at least one value is different from target. return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value <> ?)", - []interface{}{tagFilter.Key, value} + []interface{}{tagFilter.Key, value}, nil case TagFilterOperatorContains: return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value LIKE ? ESCAPE '\\')", - []interface{}{tagFilter.Key, buildContainsPattern(value)} + []interface{}{tagFilter.Key, buildContainsPattern(value)}, nil case TagFilterOperatorDoesNotContain: // Best-practice semantics for multi-value keys: // include app when key exists and at least one value does not contain target. return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value NOT LIKE ? ESCAPE '\\')", - []interface{}{tagFilter.Key, buildContainsPattern(value)} + []interface{}{tagFilter.Key, buildContainsPattern(value)}, nil case TagFilterOperatorExists: return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ?)", - []interface{}{tagFilter.Key} + []interface{}{tagFilter.Key}, nil case TagFilterOperatorDoesNotExist: return "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ?)", - []interface{}{tagFilter.Key} + []interface{}{tagFilter.Key}, nil default: - // Invalid operator should never reach here due request validation. - // Returning false condition keeps query safe if validation is bypassed. - return "1 = 0", nil + return "", nil, fmt.Errorf("unsupported tag filter operator: %s", tagFilter.Operator) } } diff --git a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go index 5fd270b5af..eff70755dd 100644 --- a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go +++ b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder_tag_filters_test.go @@ -13,14 +13,16 @@ func stringPointer(value string) *string { func TestBuildAppListingWhereCondition_WithTagFiltersAnd(t *testing.T) { queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) - whereClause, queryParams := queryBuilder.buildAppListingWhereCondition(AppListingFilter{ - TagFilters: []TagFilter{ - {Key: "owner", Operator: TagFilterOperatorEquals, Value: stringPointer("James")}, - {Key: "env", Operator: TagFilterOperatorDoesNotContain, Value: stringPointer("pro_d%")}, - {Key: "team", Operator: TagFilterOperatorExists, Value: nil}, - {Key: "zone", Operator: TagFilterOperatorDoesNotExist, Value: nil}, - }, + tagFilters := []TagFilter{ + {Key: "owner", Operator: TagFilterOperatorEquals, Value: stringPointer("James")}, + {Key: "env", Operator: TagFilterOperatorDoesNotContain, Value: stringPointer("pro_d%")}, + {Key: "team", Operator: TagFilterOperatorExists, Value: nil}, + {Key: "zone", Operator: TagFilterOperatorDoesNotExist, Value: nil}, + } + whereClause, queryParams, err := queryBuilder.buildAppListingWhereCondition(AppListingFilter{ + TagFilters: &tagFilters, }) + require.NoError(t, err) require.Contains(t, whereClause, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?)") require.Contains(t, whereClause, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value NOT LIKE ? ESCAPE '\\')") @@ -41,11 +43,12 @@ func TestBuildTagFilterPredicate_DoesNotEqualRequiresKeyAndDifferentValue(t *tes queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) value := "mayank" - predicate, queryParams := queryBuilder.buildTagFilterPredicate(TagFilter{ + predicate, queryParams, err := queryBuilder.buildTagFilterPredicate(TagFilter{ Key: "owner", Operator: TagFilterOperatorDoesNotEqual, Value: &value, }) + require.NoError(t, err) require.Equal(t, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value <> ?)", predicate) require.Equal(t, []interface{}{"owner", "mayank"}, queryParams) @@ -55,12 +58,61 @@ func TestBuildTagFilterPredicate_DoesNotContainRequiresKeyAndNotLike(t *testing. queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) value := "may" - predicate, queryParams := queryBuilder.buildTagFilterPredicate(TagFilter{ + predicate, queryParams, err := queryBuilder.buildTagFilterPredicate(TagFilter{ Key: "owner", Operator: TagFilterOperatorDoesNotContain, Value: &value, }) + require.NoError(t, err) require.Equal(t, "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value NOT LIKE ? ESCAPE '\\')", predicate) require.Equal(t, []interface{}{"owner", "%may%"}, queryParams) } + +func TestBuildTagFilterPredicate_InvalidOperatorReturnsError(t *testing.T) { + queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) + value := "mayank" + + predicate, queryParams, err := queryBuilder.buildTagFilterPredicate(TagFilter{ + Key: "owner", + Operator: TagFilterOperator("INVALID"), + Value: &value, + }) + + require.Error(t, err) + require.Empty(t, predicate) + require.Nil(t, queryParams) +} + +func TestBuildTagFiltersWhereConditionAND_NilFiltersReturnsNoClauseAndNoParams(t *testing.T) { + queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) + + whereClause, queryParams, err := queryBuilder.buildTagFiltersWhereConditionAND(nil) + + require.NoError(t, err) + require.Empty(t, whereClause) + require.NotNil(t, queryParams) + require.Len(t, queryParams, 0) +} + +func TestBuildAppListingWhereCondition_AppNameAndTagFiltersAreAndCombined(t *testing.T) { + queryBuilder := NewAppListingRepositoryQueryBuilder(zap.NewNop().Sugar()) + tagFilters := []TagFilter{ + {Key: "owner", Operator: TagFilterOperatorEquals, Value: stringPointer("James")}, + } + + whereClause, queryParams, err := queryBuilder.buildAppListingWhereCondition(AppListingFilter{ + AppNameSearch: "demo", + TagFilters: &tagFilters, + }) + + require.NoError(t, err) + require.Contains(t, whereClause, "a.app_name like ?") + require.Contains(t, whereClause, "and EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?)") + require.Len(t, queryParams, 5) + require.Equal(t, true, queryParams[0]) + require.Equal(t, CustomApp, queryParams[1]) + require.Equal(t, "%demo%", queryParams[2]) + require.Equal(t, "owner", queryParams[3]) + require.Equal(t, "James", queryParams[4]) +} diff --git a/pkg/app/AppListingService.go b/pkg/app/AppListingService.go index c21aa5f1d1..553443d97e 100644 --- a/pkg/app/AppListingService.go +++ b/pkg/app/AppListingService.go @@ -460,9 +460,11 @@ func (impl AppListingServiceImpl) FetchAppsByEnvironmentV2(fetchAppListingReques Size: fetchAppListingRequest.Size, DeploymentGroupId: fetchAppListingRequest.DeploymentGroupId, AppStatuses: fetchAppListingRequest.AppStatuses, - TagFilters: fetchAppListingRequest.TagFilters, AppIds: fetchAppListingRequest.AppIds, } + if fetchAppListingRequest.TagFilters != nil { + appListingFilter.TagFilters = &fetchAppListingRequest.TagFilters + } _, span := otel.Tracer("appListingRepository").Start(r.Context(), "FetchAppsByEnvironment") envContainers, appSize, err := impl.appListingRepository.FetchAppsByEnvironmentV2(appListingFilter) span.End() From 2b40f10c337e1bd649960364a34ac06919454c94 Mon Sep 17 00:00:00 2001 From: mayank-devtron Date: Fri, 20 Mar 2026 17:41:50 +0530 Subject: [PATCH 14/19] chore: add logs for app listing query builder errors (cherry picked from commit 4e682d12255bc748eb18325cbeda662771cdf60b) --- .../repository/helper/AppListingRepositoryQueryBuilder.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go index f9ed6416f3..f45ab00420 100644 --- a/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go +++ b/internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go @@ -174,6 +174,7 @@ func (impl AppListingRepositoryQueryBuilder) GetQueryForAppEnvContainers(appList query := "SELECT p.environment_id , a.id AS app_id, a.app_name,p.id as pipeline_id, a.team_id ,aps.status as app_status " queryTemp, queryParams, err := impl.TestForCommonAppFilter(appListingFilter) if err != nil { + impl.logger.Errorw("error in GetQueryForAppEnvContainers while building common app filter query", "err", err, "appListingFilter", appListingFilter) return "", nil, err } query += queryTemp @@ -191,6 +192,7 @@ func (impl AppListingRepositoryQueryBuilder) CommonJoinSubQuery(appListingFilter } whereCondition, whereConditionParams, err := impl.buildAppListingWhereCondition(appListingFilter) if err != nil { + impl.logger.Errorw("error in CommonJoinSubQuery while building app listing where condition", "err", err, "appListingFilter", appListingFilter) return "", nil, err } query = query + whereCondition @@ -201,6 +203,7 @@ func (impl AppListingRepositoryQueryBuilder) CommonJoinSubQuery(appListingFilter func (impl AppListingRepositoryQueryBuilder) TestForCommonAppFilter(appListingFilter AppListingFilter) (string, []interface{}, error) { queryTemp, queryParams, err := impl.CommonJoinSubQuery(appListingFilter) if err != nil { + impl.logger.Errorw("error in TestForCommonAppFilter while building common join sub query", "err", err, "appListingFilter", appListingFilter) return "", nil, err } query := " FROM app a " + queryTemp @@ -223,6 +226,7 @@ func (impl AppListingRepositoryQueryBuilder) BuildAppListingQueryLastDeploymentT func (impl AppListingRepositoryQueryBuilder) GetAppIdsQueryWithPaginationForLastDeployedSearch(appListingFilter AppListingFilter) (string, []interface{}, error) { join, queryParams, err := impl.CommonJoinSubQuery(appListingFilter) if err != nil { + impl.logger.Errorw("error in GetAppIdsQueryWithPaginationForLastDeployedSearch while building common join sub query", "err", err, "appListingFilter", appListingFilter) return "", nil, err } countQuery := " (SELECT count(distinct(a.id)) as count FROM app a " + join + ") AS total_count " @@ -253,6 +257,7 @@ func (impl AppListingRepositoryQueryBuilder) GetAppIdsQueryWithPaginationForAppN orderByClause := impl.buildAppListingSortBy(appListingFilter) join, queryParams, err := impl.CommonJoinSubQuery(appListingFilter) if err != nil { + impl.logger.Errorw("error in GetAppIdsQueryWithPaginationForAppNameSearch while building common join sub query", "err", err, "appListingFilter", appListingFilter) return "", nil, err } countQuery := "( SELECT count(distinct(a.id)) as count FROM app a" + join + " ) as total_count" @@ -330,6 +335,7 @@ func (impl AppListingRepositoryQueryBuilder) buildAppListingWhereCondition(appLi // Each row translates to a correlated EXISTS/NOT EXISTS on app_label. tagWhereCondition, tagQueryParams, err := impl.buildTagFiltersWhereConditionAND(appListingFilter.TagFilters) if err != nil { + impl.logger.Errorw("error in buildAppListingWhereCondition while building tag filters where condition", "err", err, "appListingFilter", appListingFilter) return "", nil, err } whereCondition += tagWhereCondition From 0548faf68d104e7aec162b1a18e0ebabe376fb39 Mon Sep 17 00:00:00 2001 From: mayank-devtron Date: Mon, 23 Mar 2026 15:51:51 +0530 Subject: [PATCH 15/19] fix: correct app label filter migration sequence --- ...er_index.down.sql => 35204400_app_label_filter_index.down.sql} | 0 ...filter_index.up.sql => 35204400_app_label_filter_index.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename scripts/sql/{35504400_app_label_filter_index.down.sql => 35204400_app_label_filter_index.down.sql} (100%) rename scripts/sql/{35504400_app_label_filter_index.up.sql => 35204400_app_label_filter_index.up.sql} (100%) diff --git a/scripts/sql/35504400_app_label_filter_index.down.sql b/scripts/sql/35204400_app_label_filter_index.down.sql similarity index 100% rename from scripts/sql/35504400_app_label_filter_index.down.sql rename to scripts/sql/35204400_app_label_filter_index.down.sql diff --git a/scripts/sql/35504400_app_label_filter_index.up.sql b/scripts/sql/35204400_app_label_filter_index.up.sql similarity index 100% rename from scripts/sql/35504400_app_label_filter_index.up.sql rename to scripts/sql/35204400_app_label_filter_index.up.sql From 684875dfbb53c6c683cb6f15f174e4c02e0cc44a Mon Sep 17 00:00:00 2001 From: mayank-devtron Date: Tue, 24 Mar 2026 12:19:15 +0530 Subject: [PATCH 16/19] rename migration no --- ...er_index.down.sql => 35204600_app_label_filter_index.down.sql} | 0 ...filter_index.up.sql => 35204600_app_label_filter_index.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename scripts/sql/{35204400_app_label_filter_index.down.sql => 35204600_app_label_filter_index.down.sql} (100%) rename scripts/sql/{35204400_app_label_filter_index.up.sql => 35204600_app_label_filter_index.up.sql} (100%) diff --git a/scripts/sql/35204400_app_label_filter_index.down.sql b/scripts/sql/35204600_app_label_filter_index.down.sql similarity index 100% rename from scripts/sql/35204400_app_label_filter_index.down.sql rename to scripts/sql/35204600_app_label_filter_index.down.sql diff --git a/scripts/sql/35204400_app_label_filter_index.up.sql b/scripts/sql/35204600_app_label_filter_index.up.sql similarity index 100% rename from scripts/sql/35204400_app_label_filter_index.up.sql rename to scripts/sql/35204600_app_label_filter_index.up.sql From 8c52e280e229797a1efbb882311d1003bb84f824 Mon Sep 17 00:00:00 2001 From: systemsdt <129372406+systemsdt@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:10:03 +0530 Subject: [PATCH 17/19] release: PR for v2.1.1 (#6940) * Updated release-notes files * Updated release notes * Updated release notes * Updated devtron to 1188d0b9-434-38818 tag in values file * Updated hyperion to 1188d0b9-280-38819 tag in values file * Updated release notes * Updated dashboard to 8a175cbd-690-38843 tag in values file * Updated the version in scripts * Update release notes for version 2.1.1 * Update release notes for version 2.1.1 Removed the Enhancements and Others sections from the release notes. --------- Co-authored-by: akshatsinha007 <156403098+akshatsinha007@users.noreply.github.com> --- CHANGELOG/release-notes-v2.1.1.md | 5 +++++ charts/devtron/Chart.yaml | 4 ++-- charts/devtron/devtron-bom.yaml | 8 ++++---- charts/devtron/values.yaml | 8 ++++---- devtron-images.txt.source | 6 +++--- manifests/install/devtron-installer.yaml | 2 +- manifests/installation-script | 2 +- releasenotes.md | 15 ++++----------- 8 files changed, 24 insertions(+), 26 deletions(-) create mode 100644 CHANGELOG/release-notes-v2.1.1.md diff --git a/CHANGELOG/release-notes-v2.1.1.md b/CHANGELOG/release-notes-v2.1.1.md new file mode 100644 index 0000000000..eb34195456 --- /dev/null +++ b/CHANGELOG/release-notes-v2.1.1.md @@ -0,0 +1,5 @@ +## v2.1.1 + +## Bugs +- fix: auto assign permission group related fixes (#6934) +- fix: clusterId check for modifying triggers for cluster level notific… (#6932) diff --git a/charts/devtron/Chart.yaml b/charts/devtron/Chart.yaml index 8e0476d66d..558e0c3ba4 100644 --- a/charts/devtron/Chart.yaml +++ b/charts/devtron/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: devtron-operator -appVersion: 2.1.0 +appVersion: 2.1.1 description: Chart to configure and install Devtron. Devtron is a Kubernetes Orchestration system. keywords: - Devtron @@ -11,7 +11,7 @@ keywords: - argocd - Hyperion engine: gotpl -version: 0.23.1 +version: 0.23.2 sources: - https://github.com/devtron-labs/charts dependencies: diff --git a/charts/devtron/devtron-bom.yaml b/charts/devtron/devtron-bom.yaml index de89b6d700..d9d0fc7c46 100644 --- a/charts/devtron/devtron-bom.yaml +++ b/charts/devtron/devtron-bom.yaml @@ -15,7 +15,7 @@ global: PG_DATABASE: orchestrator extraManifests: [] installer: - release: "v2.1.0" + release: "v2.1.1" registry: "" image: "inception" tag: "473deaa4-185-21582" @@ -41,13 +41,13 @@ components: FEATURE_CODE_MIRROR_ENABLE: "true" FEATURE_GROUPED_APP_LIST_FILTERS_ENABLE: "true" registry: "" - image: "dashboard:d4a16ea7-690-38751" + image: "dashboard:8a175cbd-690-38843" imagePullPolicy: IfNotPresent healthPort: 8080 devtron: registry: "" - image: "hyperion:634eb598-280-38763" - cicdImage: "devtron:634eb598-434-38762" + image: "hyperion:1188d0b9-280-38819" + cicdImage: "devtron:1188d0b9-434-38818" imagePullPolicy: IfNotPresent customOverrides: {} podSecurityContext: diff --git a/charts/devtron/values.yaml b/charts/devtron/values.yaml index 2b32cd5fbc..3f71e8db22 100644 --- a/charts/devtron/values.yaml +++ b/charts/devtron/values.yaml @@ -42,7 +42,7 @@ nfs: extraManifests: [] installer: repo: "devtron-labs/devtron" - release: "v2.1.0" + release: "v2.1.1" registry: "" image: inception tag: 473deaa4-185-21582 @@ -97,13 +97,13 @@ components: FEATURE_CODE_MIRROR_ENABLE: "true" FEATURE_GROUPED_APP_LIST_FILTERS_ENABLE: "true" registry: "" - image: "dashboard:d4a16ea7-690-38751" + image: "dashboard:8a175cbd-690-38843" imagePullPolicy: IfNotPresent healthPort: 8080 devtron: registry: "" - image: "hyperion:634eb598-280-38763" - cicdImage: "devtron:634eb598-434-38762" + image: "hyperion:1188d0b9-280-38819" + cicdImage: "devtron:1188d0b9-434-38818" imagePullPolicy: IfNotPresent customOverrides: {} healthPort: 8080 diff --git a/devtron-images.txt.source b/devtron-images.txt.source index b3849be494..5b5010ed5a 100644 --- a/devtron-images.txt.source +++ b/devtron-images.txt.source @@ -11,13 +11,13 @@ quay.io/devtron/chart-sync:fbde4d5e-836-38757 quay.io/devtron/ci-runner:fbde4d5e-138-38754 quay.io/devtron/clair:4.3.6 quay.io/devtron/curl:7.73.0 -quay.io/devtron/dashboard:d4a16ea7-690-38751 +quay.io/devtron/dashboard:8a175cbd-690-38843 quay.io/devtron/devtron-utils:dup-chart-repo-v1.1.0 -quay.io/devtron/devtron:634eb598-434-38762 +quay.io/devtron/devtron:1188d0b9-434-38818 quay.io/devtron/dex:v2.30.2 quay.io/devtron/git-sensor:fbde4d5e-200-38750 quay.io/devtron/grafana:7.3.1 -quay.io/devtron/hyperion:634eb598-280-38763 +quay.io/devtron/hyperion:1188d0b9-280-38819 quay.io/devtron/image-scanner:fbde4d5e-141-38756 quay.io/devtron/inception:473deaa4-185-21582 quay.io/devtron/k8s-sidecar:1.1.0 diff --git a/manifests/install/devtron-installer.yaml b/manifests/install/devtron-installer.yaml index 5e168f8482..c1586e124d 100644 --- a/manifests/install/devtron-installer.yaml +++ b/manifests/install/devtron-installer.yaml @@ -4,4 +4,4 @@ metadata: name: installer-devtron namespace: devtroncd spec: - url: https://raw.githubusercontent.com/devtron-labs/devtron/v2.1.0/manifests/installation-script + url: https://raw.githubusercontent.com/devtron-labs/devtron/v2.1.1/manifests/installation-script diff --git a/manifests/installation-script b/manifests/installation-script index d68f24dfbd..a2b914b7a3 100644 --- a/manifests/installation-script +++ b/manifests/installation-script @@ -1,4 +1,4 @@ -LTAG="v2.1.0"; +LTAG="v2.1.1"; REPO_RAW_URL="https://raw.githubusercontent.com/devtron-labs/devtron/"; shebang = `#!/bin/bash `; diff --git a/releasenotes.md b/releasenotes.md index ab791210c5..5ae13f8bab 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -1,13 +1,6 @@ -## v2.1.0 +## v2.1.1 + -## Enhancements -- feat: auto assign permission group ( https://github.com/devtron-labs/devtron/issues/6911 ) ## Bugs -- fix: prevent exposure of internal-only attributes in API responses and requests (#6917) -- fix: append filtered cluster details to the cluster detail list in capacity handler (#6915) -- fix: enhance cluster overview response with raw cluster capacity details and caching support (#6914) -- fix: Handle cluster capacity fetch errors by returning detailed connection failure status (#6912) -## Others -- chore: Adds scarf pixel (#6918) -- misc: add clientIP in audit log (#6908) -- misc: Refactor vulnerability query implementation and cleanup unused code (#6907) +- fix: auto assign permission group related fixes (#6934) +- fix: clusterId check for modifying triggers for cluster level notific… (#6932) From 249bf41c0a2c93d2b04e4cfb4eeb083e5f84ee90 Mon Sep 17 00:00:00 2001 From: satya_prakash <155617493+SATYAsasini@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:06:53 +0530 Subject: [PATCH 18/19] sync: migration seq (#6942) * sync: migration files * sync: migration files --- scripts/sql/35204400_approval_slack_templates.down.sql | 0 scripts/sql/35204400_approval_slack_templates.up.sql | 0 scripts/sql/35304400_api_response_cache.down.sql | 0 scripts/sql/35304400_api_response_cache.up.sql | 0 scripts/sql/35404400_add_vulnerability_query_indexes.down.sql | 0 scripts/sql/35404400_add_vulnerability_query_indexes.up.sql | 0 scripts/sql/35504400_approval_outcome_notifications.down.sql | 0 scripts/sql/35504400_approval_outcome_notifications.up.sql | 0 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 scripts/sql/35204400_approval_slack_templates.down.sql create mode 100644 scripts/sql/35204400_approval_slack_templates.up.sql create mode 100644 scripts/sql/35304400_api_response_cache.down.sql create mode 100644 scripts/sql/35304400_api_response_cache.up.sql create mode 100644 scripts/sql/35404400_add_vulnerability_query_indexes.down.sql create mode 100644 scripts/sql/35404400_add_vulnerability_query_indexes.up.sql create mode 100644 scripts/sql/35504400_approval_outcome_notifications.down.sql create mode 100644 scripts/sql/35504400_approval_outcome_notifications.up.sql diff --git a/scripts/sql/35204400_approval_slack_templates.down.sql b/scripts/sql/35204400_approval_slack_templates.down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/sql/35204400_approval_slack_templates.up.sql b/scripts/sql/35204400_approval_slack_templates.up.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/sql/35304400_api_response_cache.down.sql b/scripts/sql/35304400_api_response_cache.down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/sql/35304400_api_response_cache.up.sql b/scripts/sql/35304400_api_response_cache.up.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/sql/35404400_add_vulnerability_query_indexes.down.sql b/scripts/sql/35404400_add_vulnerability_query_indexes.down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/sql/35404400_add_vulnerability_query_indexes.up.sql b/scripts/sql/35404400_add_vulnerability_query_indexes.up.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/sql/35504400_approval_outcome_notifications.down.sql b/scripts/sql/35504400_approval_outcome_notifications.down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/sql/35504400_approval_outcome_notifications.up.sql b/scripts/sql/35504400_approval_outcome_notifications.up.sql new file mode 100644 index 0000000000..e69de29bb2 From dad89c0241b315986c167c51164b62fdd53b7694 Mon Sep 17 00:00:00 2001 From: mayank-devtron Date: Wed, 25 Mar 2026 16:22:40 +0530 Subject: [PATCH 19/19] renamed migration no --- ...er_index.down.sql => 35604600_app_label_filter_index.down.sql} | 0 ...filter_index.up.sql => 35604600_app_label_filter_index.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename scripts/sql/{35204600_app_label_filter_index.down.sql => 35604600_app_label_filter_index.down.sql} (100%) rename scripts/sql/{35204600_app_label_filter_index.up.sql => 35604600_app_label_filter_index.up.sql} (100%) diff --git a/scripts/sql/35204600_app_label_filter_index.down.sql b/scripts/sql/35604600_app_label_filter_index.down.sql similarity index 100% rename from scripts/sql/35204600_app_label_filter_index.down.sql rename to scripts/sql/35604600_app_label_filter_index.down.sql diff --git a/scripts/sql/35204600_app_label_filter_index.up.sql b/scripts/sql/35604600_app_label_filter_index.up.sql similarity index 100% rename from scripts/sql/35204600_app_label_filter_index.up.sql rename to scripts/sql/35604600_app_label_filter_index.up.sql