diff --git a/api/auth/user/UserRestHandler.go b/api/auth/user/UserRestHandler.go index 888125149a3..3a652e2a351 100644 --- a/api/auth/user/UserRestHandler.go +++ b/api/auth/user/UserRestHandler.go @@ -21,6 +21,8 @@ import ( "encoding/json" "errors" "fmt" + "github.com/devtron-labs/devtron/pkg/auth/user/helper" + "github.com/gorilla/schema" "net/http" "strconv" "strings" @@ -44,12 +46,10 @@ type UserRestHandler interface { GetById(w http.ResponseWriter, r *http.Request) GetAll(w http.ResponseWriter, r *http.Request) DeleteUser(w http.ResponseWriter, r *http.Request) - GetAllDetailedUsers(w http.ResponseWriter, r *http.Request) FetchRoleGroupById(w http.ResponseWriter, r *http.Request) CreateRoleGroup(w http.ResponseWriter, r *http.Request) UpdateRoleGroup(w http.ResponseWriter, r *http.Request) FetchRoleGroups(w http.ResponseWriter, r *http.Request) - FetchDetailedRoleGroups(w http.ResponseWriter, r *http.Request) FetchRoleGroupsByName(w http.ResponseWriter, r *http.Request) DeleteRoleGroup(w http.ResponseWriter, r *http.Request) CheckUserRoles(w http.ResponseWriter, r *http.Request) @@ -210,9 +210,6 @@ func (handler UserRestHandlerImpl) UpdateUser(w http.ResponseWriter, r *http.Req // RBAC enforcer applying token := r.Header.Get("token") - if userInfo.EmailId == "admin" { - userInfo.EmailId = "admin@github.com/devtron-labs" - } err = handler.validator.Struct(userInfo) if err != nil { handler.logger.Errorw("validation err, UpdateUser", "err", err, "payload", userInfo) @@ -220,10 +217,6 @@ func (handler UserRestHandlerImpl) UpdateUser(w http.ResponseWriter, r *http.Req return } - if userInfo.EmailId == "admin@github.com/devtron-labs" { - userInfo.EmailId = "admin" - } - res, rolesChanged, groupsModified, restrictedGroups, err := handler.userService.UpdateUser(&userInfo, token, handler.CheckManagerAuth) if err != nil { @@ -309,6 +302,7 @@ func (handler UserRestHandlerImpl) GetById(w http.ResponseWriter, r *http.Reques } func (handler UserRestHandlerImpl) GetAll(w http.ResponseWriter, r *http.Request) { + var decoder = schema.NewDecoder() userId, err := handler.userService.GetLoggedInUser(r) if userId == 0 || err != nil { common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) @@ -362,35 +356,16 @@ func (handler UserRestHandlerImpl) GetAll(w http.ResponseWriter, r *http.Request common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) return } - res, err := handler.userService.GetAll() + req := &bean.FetchListingRequest{} + err = decoder.Decode(req, r.URL.Query()) if err != nil { - handler.logger.Errorw("service err, GetAll", "err", err) - common.WriteJsonResp(w, err, "Failed to Get", http.StatusInternalServerError) - return - } - - common.WriteJsonResp(w, err, res, http.StatusOK) -} - -func (handler UserRestHandlerImpl) GetAllDetailedUsers(w http.ResponseWriter, r *http.Request) { - userId, err := handler.userService.GetLoggedInUser(r) - if userId == 0 || err != nil { - common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) - return - } - - token := r.Header.Get("token") - isActionUserSuperAdmin := false - if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); ok { - isActionUserSuperAdmin = true - } - if !isActionUserSuperAdmin { - common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + handler.logger.Errorw("request err, GetAll", "err", err, "payload", req) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) return } - res, err := handler.userService.GetAllDetailedUsers() + res, err := handler.userService.GetAllWithFilters(req) if err != nil { - handler.logger.Errorw("service err, GetAllDetailedUsers", "err", err) + handler.logger.Errorw("service err, GetAll", "err", err) common.WriteJsonResp(w, err, "Failed to Get", http.StatusInternalServerError) return } @@ -451,7 +426,15 @@ func (handler UserRestHandlerImpl) DeleteUser(w http.ResponseWriter, r *http.Req } } //RBAC enforcer Ends - + //validation + validated := helper.CheckIfUserDevtronManaged(int32(id)) + if !validated { + err = &util.ApiError{Code: "400", HttpStatusCode: 400, UserMessage: "cannot delete system or admin user"} + handler.logger.Errorw("request err, DeleteUser, validation failed", "id", id, "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + //service call res, err := handler.userService.DeleteUser(user) if err != nil { handler.logger.Errorw("service err, DeleteUser", "err", err, "id", id) @@ -639,6 +622,7 @@ func (handler UserRestHandlerImpl) UpdateRoleGroup(w http.ResponseWriter, r *htt } func (handler UserRestHandlerImpl) FetchRoleGroups(w http.ResponseWriter, r *http.Request) { + var decoder = schema.NewDecoder() userId, err := handler.userService.GetLoggedInUser(r) if userId == 0 || err != nil { common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) @@ -692,32 +676,15 @@ func (handler UserRestHandlerImpl) FetchRoleGroups(w http.ResponseWriter, r *htt common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) return } - res, err := handler.roleGroupService.FetchRoleGroups() - if err != nil { - handler.logger.Errorw("service err, FetchRoleGroups", "err", err) - common.WriteJsonResp(w, err, "", http.StatusInternalServerError) - return - } - common.WriteJsonResp(w, err, res, http.StatusOK) -} -func (handler UserRestHandlerImpl) FetchDetailedRoleGroups(w http.ResponseWriter, r *http.Request) { - userId, err := handler.userService.GetLoggedInUser(r) - if userId == 0 || err != nil { - common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) - return - } - token := r.Header.Get("token") - isActionUserSuperAdmin := false - if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); ok { - isActionUserSuperAdmin = true - } - if !isActionUserSuperAdmin { - common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + req := &bean.FetchListingRequest{} + err = decoder.Decode(req, r.URL.Query()) + if err != nil { + handler.logger.Errorw("request err, FetchRoleGroups", "err", err, "payload", req) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) return } - - res, err := handler.roleGroupService.FetchDetailedRoleGroups() + res, err := handler.roleGroupService.FetchRoleGroupsWithFilters(req) if err != nil { handler.logger.Errorw("service err, FetchRoleGroups", "err", err) common.WriteJsonResp(w, err, "", http.StatusInternalServerError) diff --git a/api/auth/user/UserRouter.go b/api/auth/user/UserRouter.go index 0953e5c068b..c3f9b22bbe4 100644 --- a/api/auth/user/UserRouter.go +++ b/api/auth/user/UserRouter.go @@ -49,9 +49,6 @@ func (router UserRouterImpl) InitUserRouter(userAuthRouter *mux.Router) { userAuthRouter.Path("/{id}"). HandlerFunc(router.userRestHandler.DeleteUser).Methods("DELETE") - userAuthRouter.Path("/detail/get"). - HandlerFunc(router.userRestHandler.GetAllDetailedUsers).Methods("GET") - userAuthRouter.Path("/role/group/{id}"). HandlerFunc(router.userRestHandler.FetchRoleGroupById).Methods("GET") userAuthRouter.Path("/role/group"). @@ -60,8 +57,6 @@ func (router UserRouterImpl) InitUserRouter(userAuthRouter *mux.Router) { HandlerFunc(router.userRestHandler.UpdateRoleGroup).Methods("PUT") userAuthRouter.Path("/role/group"). HandlerFunc(router.userRestHandler.FetchRoleGroups).Methods("GET") - userAuthRouter.Path("/role/group/detailed/get"). - HandlerFunc(router.userRestHandler.FetchDetailedRoleGroups).Methods("GET") userAuthRouter.Path("/role/group/search"). Queries("name", "{name}"). HandlerFunc(router.userRestHandler.FetchRoleGroupsByName).Methods("GET") diff --git a/api/auth/user/wire_user.go b/api/auth/user/wire_user.go index 7dea5ff8162..ee6cf646541 100644 --- a/api/auth/user/wire_user.go +++ b/api/auth/user/wire_user.go @@ -5,6 +5,7 @@ import ( "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin" user2 "github.com/devtron-labs/devtron/pkg/auth/user" repository2 "github.com/devtron-labs/devtron/pkg/auth/user/repository" + "github.com/devtron-labs/devtron/pkg/auth/user/repository/helper" "github.com/google/wire" ) @@ -12,7 +13,7 @@ import ( var UserWireSet = wire.NewSet( UserAuditWireSet, - + helper.NewUserRepositoryQueryBuilder, NewUserAuthRouterImpl, wire.Bind(new(UserAuthRouter), new(*UserAuthRouterImpl)), NewUserAuthHandlerImpl, diff --git a/api/bean/UserRequest.go b/api/bean/UserRequest.go index 404ef0d9d1c..f0dca72d917 100644 --- a/api/bean/UserRequest.go +++ b/api/bean/UserRequest.go @@ -19,6 +19,7 @@ package bean import ( "encoding/json" + "github.com/devtron-labs/devtron/pkg/auth/user/bean" "time" ) @@ -29,19 +30,20 @@ type UserRole struct { } type UserInfo struct { - Id int32 `json:"id" validate:"number"` - EmailId string `json:"email_id" validate:"required"` - Roles []string `json:"roles,omitempty"` - AccessToken string `json:"access_token,omitempty"` - UserType string `json:"-"` - LastUsedAt time.Time `json:"-"` - LastUsedByIp string `json:"-"` - Exist bool `json:"-"` - UserId int32 `json:"-"` // created or modified user id - RoleFilters []RoleFilter `json:"roleFilters"` - Status string `json:"status,omitempty"` - Groups []string `json:"groups"` - SuperAdmin bool `json:"superAdmin,notnull"` + Id int32 `json:"id" validate:"number,not-system-admin-userid"` + EmailId string `json:"emailId" validate:"required,not-system-admin-user"` + Roles []string `json:"roles,omitempty"` + AccessToken string `json:"access_token,omitempty"` + RoleFilters []RoleFilter `json:"roleFilters"` + Status string `json:"status,omitempty"` + Groups []string `json:"groups"` // this will be deprecated in future do not use + SuperAdmin bool `json:"superAdmin,notnull"` + LastLoginTime time.Time `json:"lastLoginTime"` + UserType string `json:"-"` + LastUsedAt time.Time `json:"-"` + LastUsedByIp string `json:"-"` + Exist bool `json:"-"` + UserId int32 `json:"-"` // created or modified user id } type RoleGroup struct { @@ -117,3 +119,23 @@ const ( CHART_GROUP_ENTITY = "chart-group" CLUSTER_ENTITIY = "cluster" ) + +type UserListingResponse struct { + Users []UserInfo `json:"users"` + TotalCount int `json:"totalCount"` +} + +type RoleGroupListingResponse struct { + RoleGroups []*RoleGroup `json:"roleGroups"` + TotalCount int `json:"totalCount"` +} + +type FetchListingRequest struct { + SearchKey string `json:"searchKey"` + SortOrder bean.SortOrder `json:"sortOrder"` + SortBy bean.SortBy `json:"sortBy"` + Offset int `json:"offset"` + Size int `json:"size"` + ShowAll bool `json:"showAll"` + CountCheck bool `json:"-"` +} diff --git a/cmd/external-app/wire_gen.go b/cmd/external-app/wire_gen.go index 639fb9d4dee..20ad0ab23ea 100644 --- a/cmd/external-app/wire_gen.go +++ b/cmd/external-app/wire_gen.go @@ -69,6 +69,7 @@ import ( "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" + "github.com/devtron-labs/devtron/pkg/auth/user/repository/helper" "github.com/devtron-labs/devtron/pkg/chartRepo" "github.com/devtron-labs/devtron/pkg/chartRepo/repository" "github.com/devtron-labs/devtron/pkg/cluster" @@ -153,7 +154,8 @@ func InitializeApp() (*App, error) { userCommonServiceImpl := user.NewUserCommonServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, sessionManager, rbacDataCacheFactoryImpl) userAuditRepositoryImpl := repository.NewUserAuditRepositoryImpl(db) userAuditServiceImpl := user.NewUserAuditServiceImpl(sugaredLogger, userAuditRepositoryImpl) - userServiceImpl := user.NewUserServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, sessionManager, userCommonServiceImpl, userAuditServiceImpl) + userRepositoryQueryBuilder := helper.NewUserRepositoryQueryBuilder(sugaredLogger) + userServiceImpl := user.NewUserServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, sessionManager, userCommonServiceImpl, userAuditServiceImpl, userRepositoryQueryBuilder) ssoLoginRepositoryImpl := sso.NewSSOLoginRepositoryImpl(db) k8sServiceImpl := k8s.NewK8sUtil(sugaredLogger, runtimeConfig) devtronSecretConfig, err := util2.GetDevtronSecretName() @@ -218,7 +220,7 @@ func InitializeApp() (*App, error) { teamRouterImpl := team2.NewTeamRouterImpl(teamRestHandlerImpl) userAuthHandlerImpl := user2.NewUserAuthHandlerImpl(userAuthServiceImpl, validate, sugaredLogger, enforcerImpl) userAuthRouterImpl := user2.NewUserAuthRouterImpl(sugaredLogger, userAuthHandlerImpl, userAuthOidcHelperImpl) - roleGroupServiceImpl := user.NewRoleGroupServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, userCommonServiceImpl) + roleGroupServiceImpl := user.NewRoleGroupServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, userCommonServiceImpl, userRepositoryQueryBuilder) userRestHandlerImpl := user2.NewUserRestHandlerImpl(userServiceImpl, validate, sugaredLogger, enforcerImpl, roleGroupServiceImpl, userCommonServiceImpl) userRouterImpl := user2.NewUserRouterImpl(userRestHandlerImpl) genericNoteRepositoryImpl := repository6.NewGenericNoteRepositoryImpl(db) diff --git a/internal/util/ValidateUtil.go b/internal/util/ValidateUtil.go index a6b14a1a788..ca3e940be1c 100644 --- a/internal/util/ValidateUtil.go +++ b/internal/util/ValidateUtil.go @@ -18,6 +18,7 @@ package util import ( + "github.com/devtron-labs/devtron/pkg/auth/user/bean" "regexp" "strings" @@ -94,9 +95,33 @@ func IntValidator() (*validator.Validate, error) { if err != nil { return v, err } + err = v.RegisterValidation("not-system-admin-user", validateForSystemOrAdminUser) + if err != nil { + return v, err + } + err = v.RegisterValidation("not-system-admin-userid", validateForSystemOrAdminUserById) + if err != nil { + return v, err + } return v, err } +func validateForSystemOrAdminUser(fl validator.FieldLevel) bool { + value := fl.Field().String() + if value == bean.AdminUser || value == bean.SystemUser { + return false + } + return true +} + +func validateForSystemOrAdminUserById(fl validator.FieldLevel) bool { + value := fl.Field().Int() + if value == bean.AdminUserId || value == bean.SystemUserId { + return false + } + return true +} + func validateDockerImage(fl validator.FieldLevel) bool { value := fl.Field().String() if strings.Contains(value, ":") { diff --git a/pkg/auth/user/RoleGroupService.go b/pkg/auth/user/RoleGroupService.go index 428b9b54287..15599bef4e1 100644 --- a/pkg/auth/user/RoleGroupService.go +++ b/pkg/auth/user/RoleGroupService.go @@ -20,6 +20,7 @@ package user import ( "errors" "fmt" + "github.com/devtron-labs/devtron/pkg/auth/user/repository/helper" "strings" "time" @@ -38,31 +39,34 @@ import ( type RoleGroupService interface { CreateRoleGroup(request *bean.RoleGroup) (*bean.RoleGroup, error) UpdateRoleGroup(request *bean.RoleGroup, token string, managerAuth func(resource, token string, object string) bool) (*bean.RoleGroup, error) - FetchDetailedRoleGroups() ([]*bean.RoleGroup, error) FetchRoleGroupsById(id int32) (*bean.RoleGroup, error) - FetchRoleGroups() ([]*bean.RoleGroup, error) + FetchRoleGroups() (*bean.RoleGroupListingResponse, error) + FetchRoleGroupsWithFilters(request *bean.FetchListingRequest) (*bean.RoleGroupListingResponse, error) FetchRoleGroupsByName(name string) ([]*bean.RoleGroup, error) DeleteRoleGroup(model *bean.RoleGroup) (bool, error) FetchRolesForGroups(groupNames []string) ([]*bean.RoleFilter, error) } type RoleGroupServiceImpl struct { - userAuthRepository repository.UserAuthRepository - logger *zap.SugaredLogger - userRepository repository.UserRepository - roleGroupRepository repository.RoleGroupRepository - userCommonService UserCommonService + userAuthRepository repository.UserAuthRepository + logger *zap.SugaredLogger + userRepository repository.UserRepository + roleGroupRepository repository.RoleGroupRepository + userCommonService UserCommonService + userRepositoryQueryBuilder helper.UserRepositoryQueryBuilder } func NewRoleGroupServiceImpl(userAuthRepository repository.UserAuthRepository, logger *zap.SugaredLogger, userRepository repository.UserRepository, - roleGroupRepository repository.RoleGroupRepository, userCommonService UserCommonService) *RoleGroupServiceImpl { + roleGroupRepository repository.RoleGroupRepository, userCommonService UserCommonService, + userRepositoryQueryBuilder helper.UserRepositoryQueryBuilder) *RoleGroupServiceImpl { serviceImpl := &RoleGroupServiceImpl{ - userAuthRepository: userAuthRepository, - logger: logger, - userRepository: userRepository, - roleGroupRepository: roleGroupRepository, - userCommonService: userCommonService, + userAuthRepository: userAuthRepository, + logger: logger, + userRepository: userRepository, + roleGroupRepository: roleGroupRepository, + userCommonService: userCommonService, + userRepositoryQueryBuilder: userRepositoryQueryBuilder, } cStore = sessions.NewCookieStore(randKey()) return serviceImpl @@ -586,7 +590,7 @@ func (impl RoleGroupServiceImpl) getRoleGroupMetadata(roleGroup *repository.Role return roleFilters, isSuperAdmin } -func (impl RoleGroupServiceImpl) FetchDetailedRoleGroups() ([]*bean.RoleGroup, error) { +func (impl RoleGroupServiceImpl) fetchDetailedRoleGroups() ([]*bean.RoleGroup, error) { roleGroups, err := impl.roleGroupRepository.GetAllRoleGroup() if err != nil { impl.logger.Errorw("error while fetching user from db", "error", err) @@ -619,12 +623,50 @@ func (impl RoleGroupServiceImpl) FetchDetailedRoleGroups() ([]*bean.RoleGroup, e return list, nil } -func (impl RoleGroupServiceImpl) FetchRoleGroups() ([]*bean.RoleGroup, error) { - roleGroup, err := impl.roleGroupRepository.GetAllRoleGroup() +func (impl RoleGroupServiceImpl) FetchRoleGroups() (*bean.RoleGroupListingResponse, error) { + list, err := impl.fetchDetailedRoleGroups() if err != nil { - impl.logger.Errorw("error while fetching user from db", "error", err) + impl.logger.Errorw("error in FetchDetailedRoleGroups", "err", err) + return nil, err + } + response := &bean.RoleGroupListingResponse{ + RoleGroups: list, + TotalCount: len(list), + } + return response, nil +} + +// FetchRoleGroupsWithFilters takes listing request as input and outputs RoleGroupListingResponse based on the request filters. +func (impl RoleGroupServiceImpl) FetchRoleGroupsWithFilters(request *bean.FetchListingRequest) (*bean.RoleGroupListingResponse, error) { + // default values will be used if not provided + impl.userCommonService.SetDefaultValuesIfNotPresent(request, true) + if request.ShowAll { + return impl.FetchRoleGroups() + } + + // setting count check to true for getting only count + request.CountCheck = true + query := impl.userRepositoryQueryBuilder.GetQueryForGroupListingWithFilters(request) + totalCount, err := impl.userRepository.GetCountExecutingQuery(query) + if err != nil { + impl.logger.Errorw("error in FetchRoleGroupsWithFilters", "err", err, "query", query) return nil, err } + // setting count check to false for getting data + request.CountCheck = false + + query = impl.userRepositoryQueryBuilder.GetQueryForGroupListingWithFilters(request) + roleGroup, err := impl.roleGroupRepository.GetAllExecutingQuery(query) + if err != nil { + impl.logger.Errorw("error while FetchRoleGroupsWithFilters", "error", err, "query", query) + return nil, err + } + + response := impl.fetchRoleGroupResponseFromModel(roleGroup, totalCount) + return response, nil +} + +func (impl RoleGroupServiceImpl) fetchRoleGroupResponseFromModel(roleGroup []*repository.RoleGroup, totalCount int) *bean.RoleGroupListingResponse { var list []*bean.RoleGroup for _, item := range roleGroup { bean := &bean.RoleGroup{ @@ -639,7 +681,12 @@ func (impl RoleGroupServiceImpl) FetchRoleGroups() ([]*bean.RoleGroup, error) { if len(list) == 0 { list = make([]*bean.RoleGroup, 0) } - return list, nil + + response := &bean.RoleGroupListingResponse{ + RoleGroups: list, + TotalCount: totalCount, + } + return response } func (impl RoleGroupServiceImpl) FetchRoleGroupsByName(name string) ([]*bean.RoleGroup, error) { diff --git a/pkg/auth/user/UserAuditService.go b/pkg/auth/user/UserAuditService.go index 198aeb848b1..83a2496fec5 100644 --- a/pkg/auth/user/UserAuditService.go +++ b/pkg/auth/user/UserAuditService.go @@ -73,6 +73,7 @@ func (impl UserAuditServiceImpl) Save(userAudit *UserAudit) error { UserId: userId, ClientIp: userAudit.ClientIp, CreatedOn: userAudit.CreatedOn, + UpdatedOn: userAudit.UpdatedOn, } err := impl.userAuditRepository.Save(userAuditDb) if err != nil { diff --git a/pkg/auth/user/UserAuthService.go b/pkg/auth/user/UserAuthService.go index f04df1c1001..dd00172da26 100644 --- a/pkg/auth/user/UserAuthService.go +++ b/pkg/auth/user/UserAuthService.go @@ -469,7 +469,22 @@ func (impl UserAuthServiceImpl) AuthVerification(r *http.Request) (bool, error) HttpStatusCode: http.StatusUnauthorized, Code: constants.UserNoTokenProvided, InternalMessage: "failed to verify token", - UserMessage: fmt.Sprintf("token verification failed while getting logged in user: %s", token), + UserMessage: "token verification failed while getting logged in user", + } + return false, err + } + emailId, err := impl.userService.GetEmailFromToken(token) + if err != nil { + impl.logger.Errorw("AuthVerification failed ", "error", err) + return false, err + } + exists := impl.userService.UserExists(emailId) + if !exists { + err = &util.ApiError{ + HttpStatusCode: http.StatusUnauthorized, + Code: constants.UserNotFoundForToken, + InternalMessage: "user does not exist", + UserMessage: "active user does not exist", } return false, err } diff --git a/pkg/auth/user/UserCommonService.go b/pkg/auth/user/UserCommonService.go index 0d6d0691d6d..fd284582fca 100644 --- a/pkg/auth/user/UserCommonService.go +++ b/pkg/auth/user/UserCommonService.go @@ -29,6 +29,7 @@ type UserCommonService interface { BuildRoleFilterKeyForOtherEntity(roleFilterMap map[string]*bean.RoleFilter, role repository.RoleModel, key string) BuildRoleFilterForAllTypes(roleFilterMap map[string]*bean.RoleFilter, role repository.RoleModel, key string) GetUniqueKeyForAllEntity(role repository.RoleModel) string + SetDefaultValuesIfNotPresent(request *bean.FetchListingRequest, isRoleGroup bool) } type UserCommonServiceImpl struct { @@ -674,3 +675,16 @@ func (impl UserCommonServiceImpl) GetUniqueKeyForAllEntity(role repository.RoleM } return key } + +func (impl UserCommonServiceImpl) SetDefaultValuesIfNotPresent(request *bean.FetchListingRequest, isRoleGroup bool) { + if len(request.SortBy) == 0 { + if isRoleGroup { + request.SortBy = bean2.GroupName + } else { + request.SortBy = bean2.Email + } + } + if request.Size == 0 { + request.Size = bean2.DefaultSize + } +} diff --git a/pkg/auth/user/UserService.go b/pkg/auth/user/UserService.go index f1e647f48cb..8f596316713 100644 --- a/pkg/auth/user/UserService.go +++ b/pkg/auth/user/UserService.go @@ -20,6 +20,8 @@ package user import ( "context" "fmt" + "github.com/devtron-labs/devtron/pkg/auth/user/adapter" + "github.com/devtron-labs/devtron/pkg/auth/user/repository/helper" "net/http" "strings" "sync" @@ -52,7 +54,7 @@ type UserService interface { UpdateUser(userInfo *bean.UserInfo, token string, managerAuth func(resource, token string, object string) bool) (*bean.UserInfo, bool, bool, []string, error) GetById(id int32) (*bean.UserInfo, error) GetAll() ([]bean.UserInfo, error) - GetAllDetailedUsers() ([]bean.UserInfo, error) + GetAllWithFilters(request *bean.FetchListingRequest) (*bean.UserListingResponse, error) GetEmailFromToken(token string) (string, error) GetEmailById(userId int32) (string, error) GetLoggedInUser(r *http.Request) (int32, error) @@ -73,30 +75,33 @@ 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 + userReqState map[int32]bool + userAuthRepository repository.UserAuthRepository + logger *zap.SugaredLogger + userRepository repository.UserRepository + roleGroupRepository repository.RoleGroupRepository + sessionManager2 *middleware.SessionManager + userCommonService UserCommonService + userAuditService UserAuditService + userListingRepositoryQueryBuilder helper.UserRepositoryQueryBuilder } func NewUserServiceImpl(userAuthRepository repository.UserAuthRepository, logger *zap.SugaredLogger, userRepository repository.UserRepository, userGroupRepository repository.RoleGroupRepository, - sessionManager2 *middleware.SessionManager, userCommonService UserCommonService, userAuditService UserAuditService) *UserServiceImpl { + sessionManager2 *middleware.SessionManager, userCommonService UserCommonService, userAuditService UserAuditService, + userListingRepositoryQueryBuilder helper.UserRepositoryQueryBuilder) *UserServiceImpl { serviceImpl := &UserServiceImpl{ - userReqState: make(map[int32]bool), - userAuthRepository: userAuthRepository, - logger: logger, - userRepository: userRepository, - roleGroupRepository: userGroupRepository, - sessionManager2: sessionManager2, - userCommonService: userCommonService, - userAuditService: userAuditService, + userReqState: make(map[int32]bool), + userAuthRepository: userAuthRepository, + logger: logger, + userRepository: userRepository, + roleGroupRepository: userGroupRepository, + sessionManager2: sessionManager2, + userCommonService: userCommonService, + userAuditService: userAuditService, + userListingRepositoryQueryBuilder: userListingRepositoryQueryBuilder, } cStore = sessions.NewCookieStore(randKey()) return serviceImpl @@ -944,15 +949,90 @@ func (impl *UserServiceImpl) GetAll() ([]bean.UserInfo, error) { return response, nil } -func (impl *UserServiceImpl) GetAllDetailedUsers() ([]bean.UserInfo, error) { - models, err := impl.userRepository.GetAllExcludingApiTokenUser() +// GetAllWithFilters takes filter request gives UserListingResponse as output with some operations like filter, sorting, searching,pagination support inbuilt +func (impl UserServiceImpl) GetAllWithFilters(request *bean.FetchListingRequest) (*bean.UserListingResponse, error) { + // default values will be used if not provided + impl.userCommonService.SetDefaultValuesIfNotPresent(request, false) + if request.ShowAll { + response, err := impl.getAllDetailedUsers() + if err != nil { + impl.logger.Errorw("error in GetAllWithFilters", "err", err) + return nil, err + } + return impl.getAllDetailedUsersAdapter(response), nil + } + // setting count check to true for only count + request.CountCheck = true + // Build query from query builder + query := impl.userListingRepositoryQueryBuilder.GetQueryForUserListingWithFilters(request) + totalCount, err := impl.userRepository.GetCountExecutingQuery(query) if err != nil { - impl.logger.Errorw("error while fetching user from db", "error", err) + impl.logger.Errorw("error while fetching user from db in GetAllWithFilters", "error", err) + return nil, err + } + + // setting count check to false for getting data + request.CountCheck = false + + query = impl.userListingRepositoryQueryBuilder.GetQueryForUserListingWithFilters(request) + models, err := impl.userRepository.GetAllExecutingQuery(query) + if err != nil { + impl.logger.Errorw("error while fetching user from db in GetAllWithFilters", "error", err) + return nil, err + } + + listingResponse, err := impl.getUserResponse(models, totalCount) + if err != nil { + impl.logger.Errorw("error in GetAllWithFilters", "err", err) + return nil, err + } + + return listingResponse, nil + +} + +func (impl UserServiceImpl) getAllDetailedUsersAdapter(detailedUsers []bean.UserInfo) *bean.UserListingResponse { + listingResponse := &bean.UserListingResponse{ + Users: detailedUsers, + TotalCount: len(detailedUsers), + } + return listingResponse +} + +func (impl UserServiceImpl) getUserResponse(model []repository.UserModel, totalCount int) (*bean.UserListingResponse, error) { + var response []bean.UserInfo + for _, m := range model { + lastLoginTime := adapter.GetLastLoginTime(m) + response = append(response, bean.UserInfo{ + Id: m.Id, + EmailId: m.EmailId, + RoleFilters: make([]bean.RoleFilter, 0), + Groups: make([]string, 0), + LastLoginTime: lastLoginTime, + }) + } + if len(response) == 0 { + response = make([]bean.UserInfo, 0) + } + + listingResponse := &bean.UserListingResponse{ + Users: response, + TotalCount: totalCount, + } + return listingResponse, nil +} + +func (impl *UserServiceImpl) getAllDetailedUsers() ([]bean.UserInfo, error) { + query := impl.userListingRepositoryQueryBuilder.GetQueryForAllUserWithAudit() + models, err := impl.userRepository.GetAllExecutingQuery(query) + if err != nil { + impl.logger.Errorw("error in GetAllDetailedUsers", "err", err) return nil, err } var response []bean.UserInfo for _, model := range models { isSuperAdmin, roleFilters, filterGroups := impl.getUserMetadata(&model) + lastLoginTime := adapter.GetLastLoginTime(model) for index, roleFilter := range roleFilters { if roleFilter.Entity == "" { roleFilters[index].Entity = bean2.ENTITY_APPS @@ -962,11 +1042,12 @@ func (impl *UserServiceImpl) GetAllDetailedUsers() ([]bean.UserInfo, error) { } } response = append(response, bean.UserInfo{ - Id: model.Id, - EmailId: model.EmailId, - RoleFilters: roleFilters, - Groups: filterGroups, - SuperAdmin: isSuperAdmin, + Id: model.Id, + EmailId: model.EmailId, + RoleFilters: roleFilters, + Groups: filterGroups, + SuperAdmin: isSuperAdmin, + LastLoginTime: lastLoginTime, }) } if len(response) == 0 { @@ -1334,6 +1415,7 @@ func (impl *UserServiceImpl) saveUserAudit(r *http.Request, userId int32) { UserId: userId, ClientIp: clientIp, CreatedOn: time.Now(), + UpdatedOn: time.Now(), } impl.userAuditService.Save(userAudit) } diff --git a/pkg/auth/user/adapter/adapter.go b/pkg/auth/user/adapter/adapter.go new file mode 100644 index 00000000000..5170da3989d --- /dev/null +++ b/pkg/auth/user/adapter/adapter.go @@ -0,0 +1,14 @@ +package adapter + +import ( + "github.com/devtron-labs/devtron/pkg/auth/user/repository" + "time" +) + +func GetLastLoginTime(model repository.UserModel) time.Time { + lastLoginTime := time.Time{} + if model.UserAudit != nil { + lastLoginTime = model.UserAudit.UpdatedOn + } + return lastLoginTime +} diff --git a/pkg/auth/user/bean/bean.go b/pkg/auth/user/bean/bean.go index b4d7ea9b1ac..635b6a53ce8 100644 --- a/pkg/auth/user/bean/bean.go +++ b/pkg/auth/user/bean/bean.go @@ -50,3 +50,31 @@ type RbacPolicyEntityGroupDto struct { Entity string `json:"entity" validate:"oneof=apps cluster chart-group jobs"` AccessType string `json:"accessType,omitempty"` } + +type SortBy string +type SortOrder string + +const ( + Asc SortOrder = "ASC" + Desc SortOrder = "DESC" +) + +const ( + Email SortBy = "email_id" + LastLogin SortBy = "last_login" + GroupName SortBy = "name" +) + +const ( + DefaultSize int = 20 +) + +const ( + AdminUser string = "admin" + SystemUser string = "system" +) + +const ( + AdminUserId = 2 // we have established Admin user as 2 while setting up devtron + SystemUserId = 1 // we have established System user as 1 while setting up devtron, which are being used for auto-trigger operations +) diff --git a/pkg/auth/user/helper/helper.go b/pkg/auth/user/helper/helper.go new file mode 100644 index 00000000000..2f268578b4d --- /dev/null +++ b/pkg/auth/user/helper/helper.go @@ -0,0 +1,12 @@ +package helper + +import ( + "github.com/devtron-labs/devtron/pkg/auth/user/bean" +) + +func CheckIfUserDevtronManaged(userId int32) bool { + if userId == bean.SystemUserId || userId == bean.AdminUserId { + return false + } + return true +} diff --git a/pkg/auth/user/repository/RoleGroupRepository.go b/pkg/auth/user/repository/RoleGroupRepository.go index 6f555f3594b..ae8dd7a72bf 100644 --- a/pkg/auth/user/repository/RoleGroupRepository.go +++ b/pkg/auth/user/repository/RoleGroupRepository.go @@ -30,6 +30,7 @@ type RoleGroupRepository interface { GetRoleGroupByName(name string) (*RoleGroup, error) GetRoleGroupListByName(name string) ([]*RoleGroup, error) GetAllRoleGroup() ([]*RoleGroup, error) + GetAllExecutingQuery(query string) ([]*RoleGroup, error) GetRoleGroupListByCasbinNames(name []string) ([]*RoleGroup, error) CheckRoleGroupExistByCasbinName(name string) (bool, error) CreateRoleGroupRoleMapping(model *RoleGroupRoleMapping, tx *pg.Tx) (*RoleGroupRoleMapping, error) @@ -120,6 +121,16 @@ func (impl RoleGroupRepositoryImpl) GetAllRoleGroup() ([]*RoleGroup, error) { return model, err } +func (impl RoleGroupRepositoryImpl) GetAllExecutingQuery(query string) ([]*RoleGroup, error) { + var model []*RoleGroup + _, err := impl.dbConnection.Query(&model, query) + if err != nil { + impl.Logger.Error("error in GetAllExecutingQuery", "err", err) + return nil, err + } + return model, err +} + func (impl RoleGroupRepositoryImpl) GetRoleGroupListByCasbinNames(names []string) ([]*RoleGroup, error) { var model []*RoleGroup err := impl.dbConnection.Model(&model).Where("casbin_name in (?)", pg.In(names)).Where("active = ?", true).Select() diff --git a/pkg/auth/user/repository/UserRepository.go b/pkg/auth/user/repository/UserRepository.go index 191b296c070..3e3dc34e2fa 100644 --- a/pkg/auth/user/repository/UserRepository.go +++ b/pkg/auth/user/repository/UserRepository.go @@ -33,6 +33,7 @@ type UserRepository interface { GetById(id int32) (*UserModel, error) GetByIdIncludeDeleted(id int32) (*UserModel, error) GetAllExcludingApiTokenUser() ([]UserModel, error) + GetAllExecutingQuery(query string) ([]UserModel, error) //GetAllUserRoleMappingsForRoleId(roleId int) ([]UserRoleModel, error) FetchActiveUserByEmail(email string) (bean.UserInfo, error) FetchUserDetailByEmail(email string) (bean.UserInfo, error) @@ -41,6 +42,7 @@ type UserRepository interface { FetchUserMatchesByEmailIdExcludingApiTokenUser(email string) ([]UserModel, error) FetchActiveOrDeletedUserByEmail(email string) (*UserModel, error) UpdateRoleIdForUserRolesMappings(roleId int, newRoleId int) (*UserRoleModel, error) + GetCountExecutingQuery(query string) (int, error) } type UserRepositoryImpl struct { @@ -53,12 +55,13 @@ func NewUserRepositoryImpl(dbConnection *pg.DB, logger *zap.SugaredLogger) *User } type UserModel struct { - TableName struct{} `sql:"users"` - Id int32 `sql:"id,pk"` - EmailId string `sql:"email_id,notnull"` - AccessToken string `sql:"access_token"` - Active bool `sql:"active,notnull"` - UserType string `sql:"user_type"` + TableName struct{} `sql:"users" pg:",discard_unknown_columns"` + Id int32 `sql:"id,pk"` + EmailId string `sql:"email_id,notnull"` + AccessToken string `sql:"access_token"` + Active bool `sql:"active,notnull"` + UserType string `sql:"user_type"` + UserAudit *UserAudit `sql:"-"` sql.AuditLog } @@ -112,6 +115,16 @@ func (impl UserRepositoryImpl) GetAllExcludingApiTokenUser() ([]UserModel, error return userModel, err } +func (impl UserRepositoryImpl) GetAllExecutingQuery(query string) ([]UserModel, error) { + var userModel []UserModel + _, err := impl.dbConnection.Query(&userModel, query) + if err != nil { + impl.Logger.Error("error in GetAllExecutingQuery", "err", err, "query", query) + return nil, err + } + return userModel, err +} + func (impl UserRepositoryImpl) FetchActiveUserByEmail(email string) (bean.UserInfo, error) { var users bean.UserInfo @@ -182,3 +195,13 @@ func (impl UserRepositoryImpl) UpdateRoleIdForUserRolesMappings(roleId int, newR return &model, err } + +func (impl UserRepositoryImpl) GetCountExecutingQuery(query string) (int, error) { + var totalCount int + _, err := impl.dbConnection.Query(&totalCount, query) + if err != nil { + impl.Logger.Error("Exception caught: GetCountExecutingQuery", err) + return totalCount, err + } + return totalCount, err +} diff --git a/pkg/auth/user/repository/helper/UserRepositoryQueryBuilder.go b/pkg/auth/user/repository/helper/UserRepositoryQueryBuilder.go new file mode 100644 index 00000000000..27dda76cee4 --- /dev/null +++ b/pkg/auth/user/repository/helper/UserRepositoryQueryBuilder.go @@ -0,0 +1,85 @@ +package helper + +import ( + "fmt" + "github.com/devtron-labs/devtron/api/bean" + bean2 "github.com/devtron-labs/devtron/pkg/auth/user/bean" + "go.uber.org/zap" + "strconv" +) + +type UserRepositoryQueryBuilder struct { + logger *zap.SugaredLogger +} + +func NewUserRepositoryQueryBuilder(logger *zap.SugaredLogger) UserRepositoryQueryBuilder { + userListingRepositoryQueryBuilder := UserRepositoryQueryBuilder{ + logger: logger, + } + return userListingRepositoryQueryBuilder +} + +func (impl UserRepositoryQueryBuilder) GetQueryForUserListingWithFilters(req *bean.FetchListingRequest) string { + whereCondition := fmt.Sprintf("where active = %t AND (user_type is NULL or user_type != '%s') ", true, bean.USER_TYPE_API_TOKEN) + orderCondition := "" + + if len(req.SearchKey) > 0 { + emailIdLike := "%" + req.SearchKey + "%" + whereCondition += fmt.Sprintf("AND email_id ilike '%s' ", emailIdLike) + } + + if len(req.SortBy) > 0 && !req.CountCheck { + orderCondition += fmt.Sprintf("order by %s ", req.SortBy) + if req.SortOrder == bean2.Desc { + orderCondition += string(req.SortOrder) + } + } + + if req.Size > 0 && !req.CountCheck { + orderCondition += " limit " + strconv.Itoa(req.Size) + " offset " + strconv.Itoa(req.Offset) + "" + } + var query string + if req.CountCheck { + query = fmt.Sprintf("select count(*) from users AS user_model left join user_audit AS au on au.user_id=user_model.id %s %s;", whereCondition, orderCondition) + } else { + // have not collected client ip here. always will be empty + query = fmt.Sprintf(`SELECT "user_model".*, "user_audit"."id" AS "user_audit__id", "user_audit"."updated_on" AS "user_audit__updated_on","user_audit"."user_id" AS "user_audit__user_id" ,"user_audit"."created_on" AS "user_audit__created_on" ,"user_audit"."updated_on" AS "last_login" from users As "user_model" LEFT JOIN user_audit As "user_audit" on "user_audit"."user_id" = "user_model"."id" %s %s;`, whereCondition, orderCondition) + } + + return query +} + +func (impl UserRepositoryQueryBuilder) GetQueryForAllUserWithAudit() string { + whereCondition := fmt.Sprintf("where active = %t AND (user_type is NULL or user_type != '%s') ", true, bean.USER_TYPE_API_TOKEN) + orderCondition := fmt.Sprintf("order by user_model.updated_on %s", bean2.Desc) + query := fmt.Sprintf(`SELECT "user_model".*, "user_audit"."id" AS "user_audit__id", "user_audit"."updated_on" AS "user_audit__updated_on","user_audit"."user_id" AS "user_audit__user_id" ,"user_audit"."created_on" AS "user_audit__created_on" from users As "user_model" LEFT JOIN user_audit As "user_audit" on "user_audit"."user_id" = "user_model"."id" %s %s;`, whereCondition, orderCondition) + return query +} + +func (impl UserRepositoryQueryBuilder) GetQueryForGroupListingWithFilters(req *bean.FetchListingRequest) string { + whereCondition := fmt.Sprintf("where active = %t ", true) + orderCondition := "" + if len(req.SearchKey) > 0 { + nameIdLike := "%" + req.SearchKey + "%" + whereCondition += fmt.Sprintf("AND name ilike '%s' ", nameIdLike) + } + + if len(req.SortBy) > 0 && !req.CountCheck { + orderCondition += fmt.Sprintf("order by %s ", req.SortBy) + if req.SortOrder == bean2.Desc { + orderCondition += string(req.SortOrder) + } + } + + if req.Size > 0 && !req.CountCheck { + orderCondition += " limit " + strconv.Itoa(req.Size) + " offset " + strconv.Itoa(req.Offset) + "" + } + var query string + if req.CountCheck { + query = fmt.Sprintf("SELECT count(*) from role_group %s %s;", whereCondition, orderCondition) + } else { + query = fmt.Sprintf("SELECT * from role_group %s %s;", whereCondition, orderCondition) + } + return query + +} diff --git a/scripts/sql/222_user_status.down.sql b/scripts/sql/222_user_status.down.sql new file mode 100644 index 00000000000..53be72c6966 --- /dev/null +++ b/scripts/sql/222_user_status.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE users DROP CONSTRAINT users_timeout_window_configuration_id_fkey; + +ALTER TABLE users DROP COLUMN timeout_window_configuration_id; + +DROP TABLE timeout_window_configuration; \ No newline at end of file diff --git a/scripts/sql/222_user_status.up.sql b/scripts/sql/222_user_status.up.sql new file mode 100644 index 00000000000..9f788304ed0 --- /dev/null +++ b/scripts/sql/222_user_status.up.sql @@ -0,0 +1,21 @@ +CREATE SEQUENCE IF NOT EXISTS id_seq_timeout_window_configuration; + + +-- Table Definition +CREATE TABLE "public"."timeout_window_configuration" ( + "id" int NOT NULL DEFAULT nextval('id_seq_timeout_window_configuration'::regclass), + "timeout_window_expression" varchar(255) NOT NULL, + "timeout_window_expression_format" int NOT NULL, + "created_on" timestamptz NOT NULL, + "created_by" int4 NOT NULL, + "updated_on" timestamptz NOT NULL, + "updated_by" int4 NOT NULL, + PRIMARY KEY ("id") +); + + +ALTER TABLE "public"."users" + ADD COLUMN "timeout_window_configuration_id" int; + +ALTER TABLE "public"."users" ADD FOREIGN KEY ("timeout_window_configuration_id") + REFERENCES "public"."timeout_window_configuration" ("id"); \ No newline at end of file diff --git a/specs/group_policy.yaml b/specs/group_policy.yaml index 4aceae4f2ba..9a61102f732 100644 --- a/specs/group_policy.yaml +++ b/specs/group_policy.yaml @@ -1,31 +1,68 @@ openapi: "3.0.0" info: version: 1.0.0 - title: Swagger Petstore - description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification - termsOfService: http://swagger.io/terms/ - contact: - name: Swagger API Team - email: apiteam@swagger.io - url: http://swagger.io - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0.html -servers: - - url: http://petstore.swagger.io/api + title: Devtron paths: /user/role/group: get: - summary: Returns all group policies + summary: Returns all role groups description: all the template group policies operationId: findGroupPolicy + parameters: + - name: searchKey + in: query + description: Search key for group listing + required: false + schema: + type: string + + - name: sortOrder + in: query + description: Sorting order (ASC or DESC) + required: false + schema: + type: string + enum: + - ASC + - DESC + + - name: sortBy + in: query + description: Sorting by name + required: false + schema: + type: string + enum: + - name + + - name: offset + in: query + description: Offset for paginating the results + required: false + schema: + type: integer + + - name: size + in: query + description: Size of the result set + required: false + schema: + type: integer + + - name: showAll + in: query + description: Show all Role groups (boolean) + required: false + schema: + type: boolean + responses: '200': description: list response content: application/json: schema: - $ref: '#/components/schemas/RoleGroup' + $ref: '#/components/schemas/RoleGroupListingResponse' default: description: unexpected error content: @@ -33,7 +70,7 @@ paths: schema: $ref: '#/components/schemas/Error' post: - summary: Creates a new Group Policy + summary: Creates a new Role Group description: create chart group api, with multiple environment in one row of policy, plus chart group additional type of policy. operationId: addGroupPolicy requestBody: @@ -57,8 +94,8 @@ paths: schema: $ref: '#/components/schemas/Error' put: - summary: update a new pet - description: Creates a new pet in the store. Duplicates are allowed + summary: update a role group + description: update a role group in the system. operationId: updateGroupPolicy requestBody: description: json as request body @@ -98,8 +135,8 @@ paths: /user/role/group/search: get: - summary: search a group policies by NAME - description: search group policy by group name + summary: search a role group by NAME + description: search role group by group name operationId: findRoleGroupByName parameters: - name: name @@ -211,4 +248,15 @@ components: description: Error code message: type: string - description: Error message \ No newline at end of file + description: Error message + + RoleGroupListingResponse: + type: object + properties: + roleGroups: + items: + $ref: '#/components/schemas/RoleGroup' + description: role groups listing + totalCount: + type: integer + description: total number of results satisfying the conditions \ No newline at end of file diff --git a/specs/user_policy.yaml b/specs/user_policy.yaml index 31841ae24cb..16fe8ef9264 100644 --- a/specs/user_policy.yaml +++ b/specs/user_policy.yaml @@ -1,16 +1,7 @@ openapi: "3.0.0" info: version: 1.0.0 - title: Swagger Petstore - description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification - termsOfService: http://swagger.io/terms/ - contact: - name: Swagger API Team - email: apiteam@swagger.io - url: http://swagger.io - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0.html + title: Devtron servers: - url: http://petstore.swagger.io/api paths: @@ -19,14 +10,63 @@ paths: summary: Returns all users description: all the template users operationId: findAllUsers + parameters: + + - name: searchKey + in: query + description: Search key for user listing + required: false + schema: + type: string + + - name: sortOrder + in: query + description: Sorting order (ASC or DESC) + required: false + schema: + type: string + enum: + - ASC + - DESC + + - name: sortBy + in: query + description: Sorting by email_id or last_login + required: false + schema: + type: string + enum: + - email_id + - last_login + + - name: offset + in: query + description: Offset for paginating the results + required: false + schema: + type: integer + + - name: size + in: query + description: Size of the result set + required: false + schema: + type: integer + + - name: showAll + in: query + description: Show all users (boolean) + required: false + schema: + type: boolean + responses: '200': description: list response content: application/json: schema: - items: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/UserListingResponse' default: description: unexpected error content: @@ -58,8 +98,8 @@ paths: schema: $ref: '#/components/schemas/Error' put: - summary: update a new pet - description: Creates a new pet in the store. Duplicates are allowed + summary: update a user + description: Updates a new user in the system operationId: updateUser requestBody: description: json as request body @@ -160,6 +200,46 @@ components: items: $ref: '#/components/schemas/roleFilter' description: role filters objects + UserListingResponse: + type: object + properties: + users: + items: + $ref: '#/components/schemas/AllUsers' + description: role filters objects + totalCount: + type: integer + description: total number of results satisfying the conditions + + AllUsers: + type: object + required: + - emailId + properties: + id: + type: integer + description: Unique id of user + emailId: + type: string + description: Unique valid email-id of user, comma separated emails ids for multiple users + groups: + type: array + items: + type: string + roleFilters: + type: array + items: + $ref: '#/components/schemas/emptyRoleFilter' + description: role filters objects + lastLogin: + type: string + format: date-time + description: user last login time + + emptyRoleFilter: + type: object + required: + - action roleFilter: diff --git a/wire_gen.go b/wire_gen.go index 991b1066450..7ceb27cd037 100644 --- a/wire_gen.go +++ b/wire_gen.go @@ -82,7 +82,7 @@ import ( "github.com/devtron-labs/devtron/internal/sql/repository/bulkUpdate" "github.com/devtron-labs/devtron/internal/sql/repository/chartConfig" repository5 "github.com/devtron-labs/devtron/internal/sql/repository/dockerRegistry" - "github.com/devtron-labs/devtron/internal/sql/repository/helper" + helper2 "github.com/devtron-labs/devtron/internal/sql/repository/helper" repository12 "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" @@ -115,6 +115,7 @@ import ( "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" + "github.com/devtron-labs/devtron/pkg/auth/user/repository/helper" "github.com/devtron-labs/devtron/pkg/bulkAction" "github.com/devtron-labs/devtron/pkg/chart" "github.com/devtron-labs/devtron/pkg/chartRepo" @@ -259,7 +260,8 @@ func InitializeApp() (*App, error) { userCommonServiceImpl := user.NewUserCommonServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, sessionManager, rbacDataCacheFactoryImpl) userAuditRepositoryImpl := repository4.NewUserAuditRepositoryImpl(db) userAuditServiceImpl := user.NewUserAuditServiceImpl(sugaredLogger, userAuditRepositoryImpl) - userServiceImpl := user.NewUserServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, sessionManager, userCommonServiceImpl, userAuditServiceImpl) + userRepositoryQueryBuilder := helper.NewUserRepositoryQueryBuilder(sugaredLogger) + userServiceImpl := user.NewUserServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, sessionManager, userCommonServiceImpl, userAuditServiceImpl, userRepositoryQueryBuilder) globalEnvVariables, err := util2.GetGlobalEnvVariables() if err != nil { return nil, err @@ -474,7 +476,7 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - appListingRepositoryQueryBuilder := helper.NewAppListingRepositoryQueryBuilder(sugaredLogger) + appListingRepositoryQueryBuilder := helper2.NewAppListingRepositoryQueryBuilder(sugaredLogger) appListingRepositoryImpl := repository2.NewAppListingRepositoryImpl(sugaredLogger, db, appListingRepositoryQueryBuilder, environmentRepositoryImpl) resourceGroupRepositoryImpl := resourceGroup.NewResourceGroupRepositoryImpl(db) resourceGroupMappingRepositoryImpl := resourceGroup.NewResourceGroupMappingRepositoryImpl(db) @@ -616,7 +618,7 @@ func InitializeApp() (*App, error) { } appStoreDeploymentServiceImpl := service2.NewAppStoreDeploymentServiceImpl(sugaredLogger, installedAppRepositoryImpl, chartGroupDeploymentRepositoryImpl, appStoreApplicationVersionRepositoryImpl, environmentRepositoryImpl, clusterInstalledAppsRepositoryImpl, appRepositoryImpl, eaModeDeploymentServiceImpl, fullModeDeploymentServiceImpl, environmentServiceImpl, clusterServiceImplExtended, helmAppServiceImpl, appStoreDeploymentCommonServiceImpl, installedAppVersionHistoryRepositoryImpl, serviceDeploymentServiceTypeConfig, acdConfig, gitOpsConfigReadServiceImpl) applicationStatusHandlerImpl := pubsub.NewApplicationStatusHandlerImpl(sugaredLogger, pubSubClientServiceImpl, appServiceImpl, workflowDagExecutorImpl, installedAppDBExtendedServiceImpl, appStoreDeploymentServiceImpl, pipelineBuilderImpl, pipelineRepositoryImpl, installedAppRepositoryImpl) - roleGroupServiceImpl := user.NewRoleGroupServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, userCommonServiceImpl) + roleGroupServiceImpl := user.NewRoleGroupServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, userCommonServiceImpl, userRepositoryQueryBuilder) userRestHandlerImpl := user2.NewUserRestHandlerImpl(userServiceImpl, validate, sugaredLogger, enforcerImpl, roleGroupServiceImpl, userCommonServiceImpl) userRouterImpl := user2.NewUserRouterImpl(userRestHandlerImpl) chartRefRestHandlerImpl := restHandler.NewChartRefRestHandlerImpl(sugaredLogger, chartRefServiceImpl, chartServiceImpl)