diff --git a/api/apiToken/ApiTokenRestHandler.go b/api/apiToken/ApiTokenRestHandler.go index 40b51ebebb..14c8371f67 100644 --- a/api/apiToken/ApiTokenRestHandler.go +++ b/api/apiToken/ApiTokenRestHandler.go @@ -212,8 +212,8 @@ func (impl ApiTokenRestHandlerImpl) DeleteApiToken(w http.ResponseWriter, r *htt common.WriteJsonResp(w, err, res, http.StatusOK) } -func (handler ApiTokenRestHandlerImpl) checkManagerAuth(token string, object string) bool { - if ok := handler.enforcer.Enforce(token, casbin.ResourceUser, casbin.ActionUpdate, strings.ToLower(object)); !ok { +func (handler ApiTokenRestHandlerImpl) checkManagerAuth(resource, token, object string) bool { + if ok := handler.enforcer.Enforce(token, resource, casbin.ActionUpdate, strings.ToLower(object)); !ok { return false } return true diff --git a/api/bean/UserRequest.go b/api/bean/UserRequest.go index 4469816251..2ad7046141 100644 --- a/api/bean/UserRequest.go +++ b/api/bean/UserRequest.go @@ -60,6 +60,12 @@ type RoleFilter struct { Environment string `json:"environment"` Action string `json:"action"` AccessType string `json:"accessType"` + + Cluster string `json:"cluster"` + Namespace string `json:"namespace"` + Group string `json:"group"` + Kind string `json:"kind"` + Resource string `json:"resource"` } type Role struct { @@ -76,6 +82,12 @@ type RoleData struct { Environment string `json:"environment"` Action string `json:"action"` AccessType string `json:"accessType"` + + Cluster string `json:"cluster"` + Namespace string `json:"namespace"` + Group string `json:"group"` + Kind string `json:"kind"` + Resource string `json:"resource"` } type SSOLoginDto struct { @@ -95,11 +107,11 @@ const ( type PolicyType int const ( - POLICY_DIRECT PolicyType = 1 - POLICY_GROUP PolicyType = 1 + POLICY_DIRECT PolicyType = 1 + POLICY_GROUP PolicyType = 1 + SUPERADMIN = "role:super-admin___" + APP_ACCESS_TYPE_HELM = "helm-app" + USER_TYPE_API_TOKEN = "apiToken" + CHART_GROUP_ENTITY = "chart-group" + CLUSTER_ENTITIY = "cluster" ) - -const SUPERADMIN = "role:super-admin___" -const APP_ACCESS_TYPE_HELM = "helm-app" - -const USER_TYPE_API_TOKEN = "apiToken" diff --git a/api/cluster/ClusterRestHandler.go b/api/cluster/ClusterRestHandler.go index d7b404168b..d9bf6d0d05 100644 --- a/api/cluster/ClusterRestHandler.go +++ b/api/cluster/ClusterRestHandler.go @@ -51,7 +51,9 @@ type ClusterRestHandler interface { FindAllForAutoComplete(w http.ResponseWriter, r *http.Request) DeleteCluster(w http.ResponseWriter, r *http.Request) + GetClusterNamespaces(w http.ResponseWriter, r *http.Request) GetAllClusterNamespaces(w http.ResponseWriter, r *http.Request) + FindAllForClusterPermission(w http.ResponseWriter, r *http.Request) } type ClusterRestHandlerImpl struct { @@ -376,3 +378,68 @@ func (impl ClusterRestHandlerImpl) GetAllClusterNamespaces(w http.ResponseWriter common.WriteJsonResp(w, nil, clusterNamespaces, http.StatusOK) } + +func (impl ClusterRestHandlerImpl) GetClusterNamespaces(w http.ResponseWriter, r *http.Request) { + //token := r.Header.Get("token") + vars := mux.Vars(r) + clusterIdString := vars["clusterId"] + + userId, err := impl.userService.GetLoggedInUser(r) + if userId == 0 || err != nil { + impl.logger.Errorw("user not authorized", "error", err, "userId", userId) + common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) + return + } + token := r.Header.Get("token") + isActionUserSuperAdmin := false + if ok := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); ok { + isActionUserSuperAdmin = true + } + clusterId, err := strconv.Atoi(clusterIdString) + if err != nil { + impl.logger.Errorw("failed to extract clusterId from param", "error", err, "clusterId", clusterIdString) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + + allClusterNamespaces, err := impl.clusterService.FindAllNamespacesByUserIdAndClusterId(userId, clusterId, isActionUserSuperAdmin) + if err != nil { + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + common.WriteJsonResp(w, nil, allClusterNamespaces, http.StatusOK) +} + +func (impl ClusterRestHandlerImpl) FindAllForClusterPermission(w http.ResponseWriter, r *http.Request) { + userId, err := impl.userService.GetLoggedInUser(r) + if userId == 0 || err != nil { + impl.logger.Errorw("user not authorized", "error", err, "userId", userId) + common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) + return + } + token := r.Header.Get("token") + isActionUserSuperAdmin := false + if ok := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); ok { + isActionUserSuperAdmin = true + } + clusterList, err := impl.clusterService.FindAllForClusterByUserId(userId, isActionUserSuperAdmin) + if err != nil { + impl.logger.Errorw("error in deleting cluster", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + // RBAC enforcer applying + // Already applied at service layer + //RBAC enforcer Ends + + if len(clusterList) == 0 { + // assumption is that if list is empty, then it can happen only in case of Unauthorized (but not sending Unauthorized for super-admin user) + if isActionUserSuperAdmin { + clusterList = make([]cluster.ClusterBean, 0) + } else { + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + return + } + } + common.WriteJsonResp(w, err, clusterList, http.StatusOK) +} diff --git a/api/cluster/ClusterRouter.go b/api/cluster/ClusterRouter.go index bb6312c98b..400a0c1792 100644 --- a/api/cluster/ClusterRouter.go +++ b/api/cluster/ClusterRouter.go @@ -57,6 +57,10 @@ func (impl ClusterRouterImpl) InitClusterRouter(clusterRouter *mux.Router) { Methods("GET"). HandlerFunc(impl.clusterRestHandler.FindAllForAutoComplete) + clusterRouter.Path("/namespaces/{clusterId}"). + Methods("GET"). + HandlerFunc(impl.clusterRestHandler.GetClusterNamespaces) + clusterRouter.Path("/namespaces"). Methods("GET"). HandlerFunc(impl.clusterRestHandler.GetAllClusterNamespaces) @@ -64,4 +68,8 @@ func (impl ClusterRouterImpl) InitClusterRouter(clusterRouter *mux.Router) { clusterRouter.Path(""). Methods("DELETE"). HandlerFunc(impl.clusterRestHandler.DeleteCluster) + + clusterRouter.Path("/auth-list"). + Methods("GET"). + HandlerFunc(impl.clusterRestHandler.FindAllForClusterPermission) } diff --git a/api/user/UserRestHandler.go b/api/user/UserRestHandler.go index 374b8091bc..62ab99ae4f 100644 --- a/api/user/UserRestHandler.go +++ b/api/user/UserRestHandler.go @@ -64,21 +64,24 @@ type userNamePassword struct { } type UserRestHandlerImpl struct { - userService user.UserService - validator *validator.Validate - logger *zap.SugaredLogger - enforcer casbin.Enforcer - roleGroupService user.RoleGroupService + userService user.UserService + validator *validator.Validate + logger *zap.SugaredLogger + enforcer casbin.Enforcer + roleGroupService user.RoleGroupService + userCommonService user.UserCommonService } func NewUserRestHandlerImpl(userService user.UserService, validator *validator.Validate, - logger *zap.SugaredLogger, enforcer casbin.Enforcer, roleGroupService user.RoleGroupService) *UserRestHandlerImpl { + logger *zap.SugaredLogger, enforcer casbin.Enforcer, roleGroupService user.RoleGroupService, + userCommonService user.UserCommonService) *UserRestHandlerImpl { userAuthHandler := &UserRestHandlerImpl{ - userService: userService, - validator: validator, - logger: logger, - enforcer: enforcer, - roleGroupService: roleGroupService, + userService: userService, + validator: validator, + logger: logger, + enforcer: enforcer, + roleGroupService: roleGroupService, + userCommonService: userCommonService, } return userAuthHandler } @@ -118,6 +121,12 @@ func (handler UserRestHandlerImpl) CreateUser(w http.ResponseWriter, r *http.Req return } } + if filter.Entity == bean.CLUSTER_ENTITIY { + if ok := handler.userCommonService.CheckRbacForClusterEntity(filter.Cluster, filter.Namespace, filter.Group, filter.Kind, filter.Resource, token, handler.CheckManagerAuth); !ok { + response.WriteResponse(http.StatusForbidden, "FORBIDDEN", w, errors.New("unauthorized")) + return + } + } } } else { if ok := handler.enforcer.Enforce(token, casbin.ResourceUser, casbin.ActionCreate, "*"); !ok { @@ -274,6 +283,11 @@ func (handler UserRestHandlerImpl) GetById(w http.ResponseWriter, r *http.Reques authPass = false } } + if filter.Entity == bean.CLUSTER_ENTITIY { + if ok := handler.userCommonService.CheckRbacForClusterEntity(filter.Cluster, filter.Namespace, filter.Group, filter.Kind, filter.Resource, token, handler.CheckManagerAuth); !ok { + authPass = false + } + } if authPass { filteredRoleFilter = append(filteredRoleFilter, filter) } @@ -332,6 +346,12 @@ func (handler UserRestHandlerImpl) GetAll(w http.ResponseWriter, r *http.Request break } } + if filter.Entity == bean.CLUSTER_ENTITIY { + if ok := handler.userCommonService.CheckRbacForClusterEntity(filter.Cluster, filter.Namespace, filter.Group, filter.Kind, filter.Resource, token, handler.CheckManagerAuth); ok { + isAuthorised = true + break + } + } } } } @@ -414,6 +434,12 @@ func (handler UserRestHandlerImpl) DeleteUser(w http.ResponseWriter, r *http.Req return } } + if filter.Entity == bean.CLUSTER_ENTITIY { + if ok := handler.userCommonService.CheckRbacForClusterEntity(filter.Cluster, filter.Namespace, filter.Group, filter.Kind, filter.Resource, token, handler.CheckManagerAuth); !ok { + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + return + } + } } } else { if ok := handler.enforcer.Enforce(token, casbin.ResourceUser, casbin.ActionDelete, ""); !ok { @@ -461,6 +487,11 @@ func (handler UserRestHandlerImpl) FetchRoleGroupById(w http.ResponseWriter, r * authPass = false } } + if filter.Entity == bean.CLUSTER_ENTITIY { + if isValidAuth := handler.userCommonService.CheckRbacForClusterEntity(filter.Cluster, filter.Namespace, filter.Group, filter.Kind, filter.Resource, token, handler.CheckManagerAuth); !isValidAuth { + authPass = false + } + } if authPass { filteredRoleFilter = append(filteredRoleFilter, filter) } @@ -507,6 +538,12 @@ func (handler UserRestHandlerImpl) CreateRoleGroup(w http.ResponseWriter, r *htt return } } + if filter.Entity == bean.CLUSTER_ENTITIY && !isActionUserSuperAdmin { + if isValidAuth := handler.userCommonService.CheckRbacForClusterEntity(filter.Cluster, filter.Namespace, filter.Group, filter.Kind, filter.Resource, token, handler.CheckManagerAuth); !isValidAuth { + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + return + } + } } } else { if ok := handler.enforcer.Enforce(token, casbin.ResourceUser, casbin.ActionCreate, "*"); !ok { @@ -616,6 +653,13 @@ func (handler UserRestHandlerImpl) FetchRoleGroups(w http.ResponseWriter, r *htt break } } + if filter.Entity == bean.CLUSTER_ENTITIY { + if isValidAuth := handler.userCommonService.CheckRbacForClusterEntity(filter.Cluster, filter.Namespace, filter.Group, filter.Kind, filter.Resource, token, handler.CheckManagerAuth); isValidAuth { + isAuthorised = true + break + } + } + } } } @@ -714,6 +758,12 @@ func (handler UserRestHandlerImpl) DeleteRoleGroup(w http.ResponseWriter, r *htt return } } + if filter.Entity == bean.CLUSTER_ENTITIY { + if isValidAuth := handler.userCommonService.CheckRbacForClusterEntity(filter.Cluster, filter.Namespace, filter.Group, filter.Kind, filter.Resource, token, handler.CheckManagerAuth); !isValidAuth { + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + return + } + } } } //RBAC enforcer Ends @@ -881,8 +931,8 @@ func (handler UserRestHandlerImpl) InvalidateRoleCache(w http.ResponseWriter, r } -func (handler UserRestHandlerImpl) CheckManagerAuth(token string, object string) bool { - if ok := handler.enforcer.Enforce(token, casbin.ResourceUser, casbin.ActionUpdate, strings.ToLower(object)); !ok { +func (handler UserRestHandlerImpl) CheckManagerAuth(resource, token string, object string) bool { + if ok := handler.enforcer.Enforce(token, resource, casbin.ActionUpdate, strings.ToLower(object)); !ok { return false } return true diff --git a/client/k8s/application/Application.go b/client/k8s/application/Application.go index 08c0ce1349..0c9f166ec0 100644 --- a/client/k8s/application/Application.go +++ b/client/k8s/application/Application.go @@ -11,11 +11,14 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" v1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" "k8s.io/utils/pointer" + "net/http" + "strings" ) type K8sClientService interface { @@ -25,6 +28,9 @@ type K8sClientService interface { DeleteResource(restConfig *rest.Config, request *K8sRequestBean) (resp *ManifestResponse, err error) ListEvents(restConfig *rest.Config, request *K8sRequestBean) (*EventsResponse, error) GetPodLogs(restConfig *rest.Config, request *K8sRequestBean) (io.ReadCloser, error) + GetApiResources(restConfig *rest.Config, includeOnlyVerb string) ([]*K8sApiResource, error) + GetResourceList(restConfig *rest.Config, request *K8sRequestBean) (*ResourceListResponse, bool, error) + ApplyResource(restConfig *rest.Config, request *K8sRequestBean, manifest string) (*ManifestResponse, error) } type K8sClientServiceImpl struct { @@ -66,6 +72,10 @@ type EventsResponse struct { Events *apiv1.EventList `json:"events,omitempty"` } +type ResourceListResponse struct { + Resources unstructured.UnstructuredList `json:"resources,omitempty"` +} + func (impl K8sClientServiceImpl) GetResource(restConfig *rest.Config, request *K8sRequestBean) (*ManifestResponse, error) { resourceIf, namespaced, err := impl.GetResourceIf(restConfig, request) if err != nil { @@ -251,6 +261,36 @@ func (impl K8sClientServiceImpl) GetResourceIf(restConfig *rest.Config, request return dynamicIf.Resource(resource), apiResource.Namespaced, nil } +func (impl K8sClientServiceImpl) GetResourceIfWithAcceptHeader(restConfig *rest.Config, request *K8sRequestBean) (resourceIf dynamic.NamespaceableResourceInterface, namespaced bool, err error) { + resourceIdentifier := request.ResourceIdentifier + discoveryClient, err := discovery.NewDiscoveryClientForConfig(restConfig) + if err != nil { + impl.logger.Errorw("error in getting k8s client", "err", err) + return nil, false, err + } + apiResource, err := ServerResourceForGroupVersionKind(discoveryClient, resourceIdentifier.GroupVersionKind) + if err != nil { + impl.logger.Errorw("error in getting server resource", "err", err) + return nil, false, err + } + resource := resourceIdentifier.GroupVersionKind.GroupVersion().WithResource(apiResource.Name) + wt := restConfig.WrapTransport // Reference: https://github.com/kubernetes/client-go/issues/407 + restConfig.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + if wt != nil { + rt = wt(rt) + } + return &HeaderAdder{ + rt: rt, + } + } + dynamicIf, err := dynamic.NewForConfig(restConfig) + if err != nil { + impl.logger.Errorw("error in getting dynamic interface for resource", "err", err) + return nil, false, err + } + return dynamicIf.Resource(resource), apiResource.Namespaced, nil +} + func ServerResourceForGroupVersionKind(discoveryClient discovery.DiscoveryInterface, gvk schema.GroupVersionKind) (*metav1.APIResource, error) { resources, err := discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) if err != nil { @@ -263,3 +303,107 @@ func ServerResourceForGroupVersionKind(discoveryClient discovery.DiscoveryInterf } return nil, errors.NewNotFound(schema.GroupResource{Group: gvk.Group, Resource: gvk.Kind}, "") } + +// if verb is supplied empty, that means - return all +func (impl K8sClientServiceImpl) GetApiResources(restConfig *rest.Config, includeOnlyVerb string) ([]*K8sApiResource, error) { + discoveryClient, err := discovery.NewDiscoveryClientForConfig(restConfig) + if err != nil { + impl.logger.Errorw("error in getting dynamic k8s client", "err", err) + return nil, err + } + + apiResourcesListFromK8s, err := discoveryClient.ServerPreferredResources() + if err != nil { + impl.logger.Errorw("error in getting api-resources from k8s", "err", err) + return nil, err + } + + apiResources := make([]*K8sApiResource, 0) + for _, apiResourceListFromK8s := range apiResourcesListFromK8s { + if apiResourceListFromK8s != nil { + for _, apiResourceFromK8s := range apiResourceListFromK8s.APIResources { + var includeResource bool + if len(includeOnlyVerb) > 0 { + for _, verb := range apiResourceFromK8s.Verbs { + if verb == includeOnlyVerb { + includeResource = true + break + } + } + } else { + includeResource = true + } + if !includeResource { + continue + } + var group string + var version string + gv := apiResourceListFromK8s.GroupVersion + if len(gv) > 0 { + splitGv := strings.Split(gv, "/") + if len(splitGv) == 1 { + version = splitGv[0] + } else { + group = splitGv[0] + version = splitGv[1] + } + } + apiResources = append(apiResources, &K8sApiResource{ + Gvk: schema.GroupVersionKind{ + Group: group, + Version: version, + Kind: apiResourceFromK8s.Kind, + }, + Namespaced: apiResourceFromK8s.Namespaced, + }) + } + } + } + return apiResources, nil +} + +func (impl K8sClientServiceImpl) GetResourceList(restConfig *rest.Config, request *K8sRequestBean) (*ResourceListResponse, bool, error) { + resourceIf, namespaced, err := impl.GetResourceIfWithAcceptHeader(restConfig, request) + if err != nil { + impl.logger.Errorw("error in getting dynamic interface for resource", "err", err) + return nil, namespaced, err + } + resourceIdentifier := request.ResourceIdentifier + var resp *unstructured.UnstructuredList + listOptions := metav1.ListOptions{ + TypeMeta: metav1.TypeMeta{ + Kind: resourceIdentifier.GroupVersionKind.Kind, + APIVersion: resourceIdentifier.GroupVersionKind.GroupVersion().String(), + }, + } + if len(resourceIdentifier.Namespace) > 0 && namespaced { + resp, err = resourceIf.Namespace(resourceIdentifier.Namespace).List(context.Background(), listOptions) + } else { + resp, err = resourceIf.List(context.Background(), listOptions) + } + if err != nil { + impl.logger.Errorw("error in getting resource", "err", err, "resource", resourceIdentifier) + return nil, namespaced, err + } + return &ResourceListResponse{*resp}, namespaced, nil +} + +func (impl K8sClientServiceImpl) ApplyResource(restConfig *rest.Config, request *K8sRequestBean, manifest string) (*ManifestResponse, error) { + resourceIf, namespaced, err := impl.GetResourceIf(restConfig, request) + if err != nil { + impl.logger.Errorw("error in getting dynamic interface for resource", "err", err) + return nil, err + } + resourceIdentifier := request.ResourceIdentifier + var resp *unstructured.Unstructured + if len(resourceIdentifier.Namespace) > 0 && namespaced { + resp, err = resourceIf.Namespace(resourceIdentifier.Namespace).Patch(context.Background(), resourceIdentifier.Name, types.StrategicMergePatchType, []byte(manifest), metav1.PatchOptions{FieldManager: "patch"}) + } else { + resp, err = resourceIf.Patch(context.Background(), resourceIdentifier.Name, types.StrategicMergePatchType, []byte(manifest), metav1.PatchOptions{FieldManager: "patch"}) + } + if err != nil { + impl.logger.Errorw("error in applying resource", "err", err) + return nil, err + } + return &ManifestResponse{*resp}, nil +} diff --git a/client/k8s/application/Bean.go b/client/k8s/application/Bean.go new file mode 100644 index 0000000000..ef8afdfe44 --- /dev/null +++ b/client/k8s/application/Bean.go @@ -0,0 +1,44 @@ +package application + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type GetAllApiResourcesResponse struct { + ApiResources []*K8sApiResource `json:"apiResources"` + AllowedAll bool `json:"allowedAll"` +} + +type K8sApiResource struct { + Gvk schema.GroupVersionKind `json:"gvk"` + Namespaced bool `json:"namespaced"` +} + +type ApplyResourcesRequest struct { + Manifest string `json:"manifest"` + ClusterId int `json:"clusterId"` +} + +type ApplyResourcesResponse struct { + Kind string `json:"kind"` + Name string `json:"name"` + Error string `json:"error"` + IsUpdate bool `json:"isUpdate"` +} + +type ClusterResourceListMap struct { + Headers []string `json:"headers"` + Data []map[string]interface{} `json:"data"` +} + +const K8sClusterResourceNameKey = "name" +const K8sClusterResourcePriorityKey = "priority" +const K8sClusterResourceNamespaceKey = "namespace" +const K8sClusterResourceMetadataKey = "metadata" +const K8sClusterResourceMetadataNameKey = "name" +const K8sClusterResourceCreationTimestampKey = "creationTimestamp" + +const K8sClusterResourceObjectKey = "object" +const K8sClusterResourceRowsKey = "rows" +const K8sClusterResourceCellKey = "cells" +const K8sClusterResourceColumnDefinitionKey = "columnDefinitions" diff --git a/client/k8s/application/Http.go b/client/k8s/application/Http.go new file mode 100644 index 0000000000..9e8d822435 --- /dev/null +++ b/client/k8s/application/Http.go @@ -0,0 +1,14 @@ +package application + +import ( + "net/http" +) + +type HeaderAdder struct { + rt http.RoundTripper +} + +func (h *HeaderAdder) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Set("Accept", "application/json;as=Table;g=meta.k8s.io;v=v1") + return h.rt.RoundTrip(req) +} diff --git a/cmd/external-app/wire_gen.go b/cmd/external-app/wire_gen.go index c62b54f6f6..3066393779 100644 --- a/cmd/external-app/wire_gen.go +++ b/cmd/external-app/wire_gen.go @@ -145,7 +145,7 @@ func InitializeApp() (*App, error) { clusterRepositoryImpl := repository2.NewClusterRepositoryImpl(db, sugaredLogger) v := informer.NewGlobalMapClusterNamespace() k8sInformerFactoryImpl := informer.NewK8sInformerFactoryImpl(sugaredLogger, v, runtimeConfig) - clusterServiceImpl := cluster.NewClusterServiceImpl(clusterRepositoryImpl, sugaredLogger, k8sUtil, k8sInformerFactoryImpl) + clusterServiceImpl := cluster.NewClusterServiceImpl(clusterRepositoryImpl, sugaredLogger, k8sUtil, k8sInformerFactoryImpl, userAuthRepositoryImpl, userRepositoryImpl, roleGroupRepositoryImpl) environmentRepositoryImpl := repository2.NewEnvironmentRepositoryImpl(db) environmentServiceImpl := cluster.NewEnvironmentServiceImpl(environmentRepositoryImpl, clusterServiceImpl, sugaredLogger, k8sUtil, k8sInformerFactoryImpl, userAuthServiceImpl) chartRepoRepositoryImpl := chartRepoRepository.NewChartRepoRepositoryImpl(db) @@ -166,7 +166,7 @@ func InitializeApp() (*App, error) { userAuthHandlerImpl := user2.NewUserAuthHandlerImpl(userAuthServiceImpl, validate, sugaredLogger) userAuthRouterImpl := user2.NewUserAuthRouterImpl(sugaredLogger, userAuthHandlerImpl, userAuthOidcHelperImpl) roleGroupServiceImpl := user.NewRoleGroupServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, userCommonServiceImpl) - userRestHandlerImpl := user2.NewUserRestHandlerImpl(userServiceImpl, validate, sugaredLogger, enforcerImpl, roleGroupServiceImpl) + userRestHandlerImpl := user2.NewUserRestHandlerImpl(userServiceImpl, validate, sugaredLogger, enforcerImpl, roleGroupServiceImpl, userCommonServiceImpl) userRouterImpl := user2.NewUserRouterImpl(userRestHandlerImpl) helmUserServiceImpl, err := argo.NewHelmUserServiceImpl(sugaredLogger) if err != nil { @@ -200,7 +200,10 @@ func InitializeApp() (*App, error) { k8sClientServiceImpl := application.NewK8sClientServiceImpl(sugaredLogger, clusterRepositoryImpl) k8sApplicationServiceImpl := k8s.NewK8sApplicationServiceImpl(sugaredLogger, clusterServiceImpl, pumpImpl, k8sClientServiceImpl, helmAppServiceImpl, k8sUtil, acdAuthConfig) terminalSessionHandlerImpl := terminal.NewTerminalSessionHandlerImpl(environmentServiceImpl, clusterServiceImpl, sugaredLogger) - k8sApplicationRestHandlerImpl := k8s.NewK8sApplicationRestHandlerImpl(sugaredLogger, k8sApplicationServiceImpl, pumpImpl, terminalSessionHandlerImpl, enforcerImpl, enforcerUtilHelmImpl, clusterServiceImpl, helmAppServiceImpl, userServiceImpl) + appRepositoryImpl := app.NewAppRepositoryImpl(db, sugaredLogger) + ciPipelineRepositoryImpl := pipelineConfig.NewCiPipelineRepositoryImpl(db, sugaredLogger) + enforcerUtilImpl := rbac.NewEnforcerUtilImpl(sugaredLogger, teamRepositoryImpl, appRepositoryImpl, environmentRepositoryImpl, pipelineRepositoryImpl, ciPipelineRepositoryImpl, clusterRepositoryImpl) + k8sApplicationRestHandlerImpl := k8s.NewK8sApplicationRestHandlerImpl(sugaredLogger, k8sApplicationServiceImpl, pumpImpl, terminalSessionHandlerImpl, enforcerImpl, enforcerUtilHelmImpl, enforcerUtilImpl, clusterServiceImpl, helmAppServiceImpl, userServiceImpl) k8sApplicationRouterImpl := k8s.NewK8sApplicationRouterImpl(k8sApplicationRestHandlerImpl) chartRefRepositoryImpl := chartRepoRepository.NewChartRefRepositoryImpl(db) refChartDir := _wireRefChartDirValue @@ -213,9 +216,6 @@ func InitializeApp() (*App, error) { appStoreValuesServiceImpl := service2.NewAppStoreValuesServiceImpl(sugaredLogger, appStoreApplicationVersionRepositoryImpl, installedAppRepositoryImpl, appStoreVersionValuesRepositoryImpl, userServiceImpl) appStoreValuesRestHandlerImpl := appStoreValues.NewAppStoreValuesRestHandlerImpl(sugaredLogger, userServiceImpl, appStoreValuesServiceImpl) appStoreValuesRouterImpl := appStoreValues.NewAppStoreValuesRouterImpl(appStoreValuesRestHandlerImpl) - appRepositoryImpl := app.NewAppRepositoryImpl(db, sugaredLogger) - ciPipelineRepositoryImpl := pipelineConfig.NewCiPipelineRepositoryImpl(db, sugaredLogger) - enforcerUtilImpl := rbac.NewEnforcerUtilImpl(sugaredLogger, teamRepositoryImpl, appRepositoryImpl, environmentRepositoryImpl, pipelineRepositoryImpl, ciPipelineRepositoryImpl, clusterRepositoryImpl) clusterInstalledAppsRepositoryImpl := repository3.NewClusterInstalledAppsRepositoryImpl(db, sugaredLogger) appStoreDeploymentHelmServiceImpl := appStoreDeploymentTool.NewAppStoreDeploymentHelmServiceImpl(sugaredLogger, helmAppServiceImpl, appStoreApplicationVersionRepositoryImpl, environmentRepositoryImpl, helmAppClientImpl, installedAppRepositoryImpl) globalEnvVariables, err := util2.GetGlobalEnvVariables() diff --git a/internal/util/K8sUtil.go b/internal/util/K8sUtil.go index 4b9d8b6be7..5401a90ddd 100644 --- a/internal/util/K8sUtil.go +++ b/internal/util/K8sUtil.go @@ -22,8 +22,11 @@ import ( "encoding/json" error2 "errors" "flag" + "github.com/devtron-labs/devtron/client/k8s/application" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "os/user" "path/filepath" + "strings" "time" "github.com/devtron-labs/authenticator/client" @@ -491,13 +494,7 @@ func (impl K8sUtil) GetResourceInfoByLabelSelector(namespace string, labelSelect func (impl K8sUtil) GetK8sClusterRestConfig() (*rest.Config, error) { impl.logger.Debug("getting k8s rest config") if impl.runTimeConfig.LocalDevMode { - usr, err := user.Current() - if err != nil { - impl.logger.Errorw("Error while getting user current env details", "error", err) - } - kubeconfig := flag.String("read-kubeconfig", filepath.Join(usr.HomeDir, ".kube", "config"), "(optional) absolute path to the kubeconfig file") - flag.Parse() - restConfig, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) + restConfig, err := clientcmd.BuildConfigFromFlags("", *impl.kubeconfig) if err != nil { impl.logger.Errorw("Error while building kubernetes cluster rest config", "error", err) return nil, err @@ -522,3 +519,112 @@ func (impl K8sUtil) GetPodByName(namespace string, name string, client *v12.Core return pod, nil } } + +func (impl K8sUtil) BuildK8sObjectListTableData(manifest *unstructured.UnstructuredList, namespaced bool, kind string, validateResourceAccess func(namespace, resourceName string) bool) (*application.ClusterResourceListMap, error) { + clusterResourceListMap := &application.ClusterResourceListMap{} + // build headers + var headers []string + columnIndexes := make(map[int]string) + if kind == "Event" { + headers, columnIndexes = impl.getEventKindHeader() + } else { + columnDefinitionsUncast := manifest.Object[application.K8sClusterResourceColumnDefinitionKey] + if columnDefinitionsUncast != nil { + columnDefinitions := columnDefinitionsUncast.([]interface{}) + for index, cd := range columnDefinitions { + if cd == nil { + continue + } + columnMap := cd.(map[string]interface{}) + columnNameUncast := columnMap[application.K8sClusterResourceNameKey] + if columnNameUncast == nil { + continue + } + priorityUncast := columnMap[application.K8sClusterResourcePriorityKey] + if priorityUncast == nil { + continue + } + columnName := columnNameUncast.(string) + columnName = strings.ToLower(columnName) + priority := priorityUncast.(int64) + if namespaced && index == 1 { + headers = append(headers, application.K8sClusterResourceNamespaceKey) + } + if priority == 0 || (manifest.GetKind() == "Event" && columnName == "source") { + columnIndexes[index] = columnName + headers = append(headers, columnName) + } + } + } + } + + // build rows + rowsMapping := make([]map[string]interface{}, 0) + rowsDataUncast := manifest.Object[application.K8sClusterResourceRowsKey] + var resourceName string + var namespace string + var allowed bool + if rowsDataUncast != nil { + rows := rowsDataUncast.([]interface{}) + for _, row := range rows { + resourceName = "" + namespace = "" + allowed = true + rowIndex := make(map[string]interface{}) + rowMap := row.(map[string]interface{}) + cellsUncast := rowMap[application.K8sClusterResourceCellKey] + if cellsUncast == nil { + continue + } + rowCells := cellsUncast.([]interface{}) + for index, columnName := range columnIndexes { + cell := rowCells[index].(interface{}) + rowIndex[columnName] = cell + } + + // set namespace + + cellObjUncast := rowMap[application.K8sClusterResourceObjectKey] + if cellObjUncast != nil { + cellObj := cellObjUncast.(map[string]interface{}) + if cellObj != nil && cellObj[application.K8sClusterResourceMetadataKey] != nil { + metadata := cellObj[application.K8sClusterResourceMetadataKey].(map[string]interface{}) + if metadata[application.K8sClusterResourceNamespaceKey] != nil { + namespace = metadata[application.K8sClusterResourceNamespaceKey].(string) + if namespaced { + rowIndex[application.K8sClusterResourceNamespaceKey] = namespace + } + } + if metadata[application.K8sClusterResourceMetadataNameKey] != nil { + resourceName = metadata[application.K8sClusterResourceMetadataNameKey].(string) + } + } + } + if resourceName != "" { + allowed = validateResourceAccess(namespace, resourceName) + } + if allowed { + rowsMapping = append(rowsMapping, rowIndex) + } + } + } + + clusterResourceListMap.Headers = headers + clusterResourceListMap.Data = rowsMapping + impl.logger.Debugw("resource listing response", "clusterResourceListMap", clusterResourceListMap) + return clusterResourceListMap, nil +} + +func (impl K8sUtil) getEventKindHeader() ([]string, map[int]string) { + headers := []string{"type", "message", "namespace", "involved object", "source", "count", "age", "last seen"} + columnIndexes := make(map[int]string) + columnIndexes[0] = "last seen" + columnIndexes[1] = "type" + columnIndexes[2] = "namespace" + columnIndexes[3] = "involved object" + columnIndexes[5] = "source" + columnIndexes[6] = "message" + columnIndexes[7] = "age" + columnIndexes[8] = "count" + return headers, columnIndexes +} diff --git a/pkg/apiToken/ApiTokenService.go b/pkg/apiToken/ApiTokenService.go index b7b34049a8..c78c9a7f57 100644 --- a/pkg/apiToken/ApiTokenService.go +++ b/pkg/apiToken/ApiTokenService.go @@ -36,7 +36,7 @@ import ( type ApiTokenService interface { GetAllActiveApiTokens() ([]*openapi.ApiToken, error) - CreateApiToken(request *openapi.CreateApiTokenRequest, createdBy int32, managerAuth func(token string, object string) bool) (*openapi.CreateApiTokenResponse, error) + CreateApiToken(request *openapi.CreateApiTokenRequest, createdBy int32, managerAuth func(resource, token, object string) bool) (*openapi.CreateApiTokenResponse, error) UpdateApiToken(apiTokenId int, request *openapi.UpdateApiTokenRequest, updatedBy int32) (*openapi.UpdateApiTokenResponse, error) DeleteApiToken(apiTokenId int, deletedBy int32) (*openapi.ActionResponse, error) GetAllApiTokensForWebhook(projectName string, environmentName string, appName string, auth func(token string, projectObject string, envObject string) bool) ([]*openapi.ApiToken, error) @@ -155,7 +155,7 @@ func (impl ApiTokenServiceImpl) GetAllActiveApiTokens() ([]*openapi.ApiToken, er return apiTokens, nil } -func (impl ApiTokenServiceImpl) CreateApiToken(request *openapi.CreateApiTokenRequest, createdBy int32, managerAuth func(token string, object string) bool) (*openapi.CreateApiTokenResponse, error) { +func (impl ApiTokenServiceImpl) CreateApiToken(request *openapi.CreateApiTokenRequest, createdBy int32, managerAuth func(resource, token string, object string) bool) (*openapi.CreateApiTokenResponse, error) { impl.logger.Infow("Creating API token", "request", request, "createdBy", createdBy) name := request.GetName() diff --git a/pkg/cluster/ClusterService.go b/pkg/cluster/ClusterService.go index cc434ab921..c7fd97193d 100644 --- a/pkg/cluster/ClusterService.go +++ b/pkg/cluster/ClusterService.go @@ -20,6 +20,8 @@ package cluster import ( "context" "fmt" + casbin2 "github.com/devtron-labs/devtron/pkg/user/casbin" + repository2 "github.com/devtron-labs/devtron/pkg/user/repository" "io/ioutil" "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -91,22 +93,33 @@ type ClusterService interface { GetClusterConfig(cluster *ClusterBean) (*util.ClusterConfig, error) GetK8sClient() (*v12.CoreV1Client, error) GetAllClusterNamespaces() map[string][]string + FindAllNamespacesByUserIdAndClusterId(userId int32, clusterId int, isActionUserSuperAdmin bool) ([]string, error) + FindAllForClusterByUserId(userId int32, isActionUserSuperAdmin bool) ([]ClusterBean, error) + FetchRolesFromGroup(userId int32) ([]*repository2.RoleModel, error) } type ClusterServiceImpl struct { - clusterRepository repository.ClusterRepository - logger *zap.SugaredLogger - K8sUtil *util.K8sUtil - K8sInformerFactory informer.K8sInformerFactory + clusterRepository repository.ClusterRepository + logger *zap.SugaredLogger + K8sUtil *util.K8sUtil + K8sInformerFactory informer.K8sInformerFactory + userAuthRepository repository2.UserAuthRepository + userRepository repository2.UserRepository + roleGroupRepository repository2.RoleGroupRepository } func NewClusterServiceImpl(repository repository.ClusterRepository, logger *zap.SugaredLogger, - K8sUtil *util.K8sUtil, K8sInformerFactory informer.K8sInformerFactory) *ClusterServiceImpl { + K8sUtil *util.K8sUtil, K8sInformerFactory informer.K8sInformerFactory, + userAuthRepository repository2.UserAuthRepository, userRepository repository2.UserRepository, + roleGroupRepository repository2.RoleGroupRepository) *ClusterServiceImpl { clusterService := &ClusterServiceImpl{ - clusterRepository: repository, - logger: logger, - K8sUtil: K8sUtil, - K8sInformerFactory: K8sInformerFactory, + clusterRepository: repository, + logger: logger, + K8sUtil: K8sUtil, + K8sInformerFactory: K8sInformerFactory, + userAuthRepository: userAuthRepository, + userRepository: userRepository, + roleGroupRepository: roleGroupRepository, } go clusterService.buildInformer() return clusterService @@ -558,3 +571,107 @@ func (impl *ClusterServiceImpl) GetAllClusterNamespaces() map[string][]string { } return result } + +func (impl *ClusterServiceImpl) FindAllNamespacesByUserIdAndClusterId(userId int32, clusterId int, isActionUserSuperAdmin bool) ([]string, error) { + result := make([]string, 0) + clusterBean, err := impl.FindById(clusterId) + if err != nil { + impl.logger.Errorw("failed to find cluster for id", "error", err, "clusterId", clusterId) + return nil, err + } + namespaceListGroupByCLuster := impl.K8sInformerFactory.GetLatestNamespaceListGroupByCLuster() + namespaces := namespaceListGroupByCLuster[clusterBean.ClusterName] + + if isActionUserSuperAdmin { + for namespace, value := range namespaces { + if value { + result = append(result, namespace) + } + } + } else { + roles, err := impl.FetchRolesFromGroup(userId) + if err != nil { + impl.logger.Errorw("error on fetching user roles for cluster list", "err", err) + return nil, err + } + allowedAll := false + allowedNamespaceMap := make(map[string]bool) + for _, role := range roles { + if clusterBean.ClusterName == role.Cluster { + allowedNamespaceMap[role.Namespace] = true + if role.Namespace == "" { + allowedAll = true + } + } + } + + //adding final namespace list + for namespace, value := range namespaces { + if _, ok := allowedNamespaceMap[namespace]; ok || allowedAll { + if value { + result = append(result, namespace) + } + } + } + } + return result, nil +} + +func (impl *ClusterServiceImpl) FindAllForClusterByUserId(userId int32, isActionUserSuperAdmin bool) ([]ClusterBean, error) { + if isActionUserSuperAdmin { + return impl.FindAllForAutoComplete() + } + allowedClustersMap := make(map[string]bool) + roles, err := impl.FetchRolesFromGroup(userId) + if err != nil { + impl.logger.Errorw("error while fetching user roles from db", "error", err) + return nil, err + } + for _, role := range roles { + allowedClustersMap[role.Cluster] = true + } + + models, err := impl.clusterRepository.FindAll() + if err != nil { + impl.logger.Errorw("error on fetching clusters", "err", err) + return nil, err + } + var beans []ClusterBean + for _, model := range models { + if _, ok := allowedClustersMap[model.ClusterName]; ok { + beans = append(beans, ClusterBean{ + Id: model.Id, + ClusterName: model.ClusterName, + }) + } + } + return beans, nil +} + +func (impl *ClusterServiceImpl) FetchRolesFromGroup(userId int32) ([]*repository2.RoleModel, error) { + user, 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 + } + roleEntity := "cluster" + roles, err := impl.userAuthRepository.GetRolesByUserIdAndEntityType(userId, roleEntity) + if err != nil { + 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...) + } + return roles, nil +} diff --git a/pkg/cluster/ClusterServiceExtended.go b/pkg/cluster/ClusterServiceExtended.go index 9e1b5bb573..375de22a18 100644 --- a/pkg/cluster/ClusterServiceExtended.go +++ b/pkg/cluster/ClusterServiceExtended.go @@ -6,6 +6,7 @@ import ( cluster3 "github.com/argoproj/argo-cd/v2/pkg/apiclient/cluster" "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" repository3 "github.com/devtron-labs/devtron/internal/sql/repository" + repository4 "github.com/devtron-labs/devtron/pkg/user/repository" "net/http" "strings" "time" @@ -35,7 +36,9 @@ type ClusterServiceImplExtended struct { func NewClusterServiceImplExtended(repository repository.ClusterRepository, environmentRepository repository.EnvironmentRepository, grafanaClient grafana.GrafanaClient, logger *zap.SugaredLogger, installedAppRepository repository2.InstalledAppRepository, K8sUtil *util.K8sUtil, - clusterServiceCD cluster2.ServiceClient, K8sInformerFactory informer.K8sInformerFactory, gitOpsRepository repository3.GitOpsConfigRepository) *ClusterServiceImplExtended { + clusterServiceCD cluster2.ServiceClient, K8sInformerFactory informer.K8sInformerFactory, + gitOpsRepository repository3.GitOpsConfigRepository, userAuthRepository repository4.UserAuthRepository, + userRepository repository4.UserRepository, roleGroupRepository repository4.RoleGroupRepository) *ClusterServiceImplExtended { clusterServiceExt := &ClusterServiceImplExtended{ environmentRepository: environmentRepository, grafanaClient: grafanaClient, @@ -43,10 +46,13 @@ func NewClusterServiceImplExtended(repository repository.ClusterRepository, envi clusterServiceCD: clusterServiceCD, gitOpsRepository: gitOpsRepository, ClusterServiceImpl: &ClusterServiceImpl{ - clusterRepository: repository, - logger: logger, - K8sUtil: K8sUtil, - K8sInformerFactory: K8sInformerFactory, + clusterRepository: repository, + logger: logger, + K8sUtil: K8sUtil, + K8sInformerFactory: K8sInformerFactory, + userAuthRepository: userAuthRepository, + userRepository: userRepository, + roleGroupRepository: roleGroupRepository, }, } go clusterServiceExt.buildInformer() diff --git a/pkg/clusterTerminalAccess/UserTerminalAccessService.go b/pkg/clusterTerminalAccess/UserTerminalAccessService.go index 0342cc0dd9..0709bae288 100644 --- a/pkg/clusterTerminalAccess/UserTerminalAccessService.go +++ b/pkg/clusterTerminalAccess/UserTerminalAccessService.go @@ -655,6 +655,7 @@ func (impl *UserTerminalAccessServiceImpl) getPodRequestBean(clusterId int, podN return nil, err } request := &k8s.ResourceRequestBean{ + ClusterId: clusterId, AppIdentifier: &client.AppIdentifier{ ClusterId: clusterId, }, diff --git a/pkg/user/RoleGroupService.go b/pkg/user/RoleGroupService.go index b3a6bb49f5..0525468678 100644 --- a/pkg/user/RoleGroupService.go +++ b/pkg/user/RoleGroupService.go @@ -34,7 +34,7 @@ import ( type RoleGroupService interface { CreateRoleGroup(request *bean.RoleGroup) (*bean.RoleGroup, error) - UpdateRoleGroup(request *bean.RoleGroup, token string, managerAuth func(token string, object string) bool) (*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) @@ -81,7 +81,6 @@ func (impl RoleGroupServiceImpl) CreateRoleGroup(request *bean.RoleGroup) (*bean return nil, err } } else { - //create new user in our db on d basis of info got from google api or hex. assign a basic role model := &repository2.RoleGroup{ Name: request.Name, @@ -111,85 +110,93 @@ func (impl RoleGroupServiceImpl) CreateRoleGroup(request *bean.RoleGroup) (*bean //Starts Role and Mapping var policies []casbin2.Policy for _, roleFilter := range request.RoleFilters { - if roleFilter.EntityName == "" { - roleFilter.EntityName = "NONE" - } - if roleFilter.Environment == "" { - roleFilter.Environment = "NONE" - } - entityNames := strings.Split(roleFilter.EntityName, ",") - environments := strings.Split(roleFilter.Environment, ",") - for _, environment := range environments { - for _, entityName := range entityNames { - if entityName == "NONE" { - entityName = "" - } - if environment == "NONE" { - environment = "" - } - roleModel, err := impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) - if err != nil { - impl.logger.Errorw("Error in fetching role by filter", "user", request) - return nil, err - } - if roleModel.Id == 0 { - impl.logger.Debugw("no role found for given filter", "filter", roleFilter) - //userInfo.Status = "role not fount for any given filter: " + roleFilter.Team + "," + roleFilter.Environment + "," + roleFilter.Application + "," + roleFilter.Action - - if len(roleFilter.Team) > 0 && len(roleFilter.Environment) > 0 { - if roleFilter.AccessType == bean.APP_ACCESS_TYPE_HELM { - flag, err := impl.userAuthRepository.CreateDefaultHelmPolicies(roleFilter.Team, entityName, environment, tx) - if err != nil || flag == false { + if roleFilter.Entity == bean.CLUSTER_ENTITIY { + policiesToBeAdded, err := impl.CreateOrUpdateRoleGroupForClusterEntity(roleFilter, request.UserId, model, nil, "", nil, tx) + policies = append(policies, policiesToBeAdded...) + if err != nil { + impl.logger.Errorw("error in creating updating role group for cluster entity", "err", err, "roleFilter", roleFilter) + } + } else { + if roleFilter.EntityName == "" { + roleFilter.EntityName = "NONE" + } + if roleFilter.Environment == "" { + roleFilter.Environment = "NONE" + } + entityNames := strings.Split(roleFilter.EntityName, ",") + environments := strings.Split(roleFilter.Environment, ",") + for _, environment := range environments { + for _, entityName := range entityNames { + if entityName == "NONE" { + entityName = "" + } + if environment == "NONE" { + environment = "" + } + roleModel, err := impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "user", request) + return nil, err + } + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + //userInfo.Status = "role not fount for any given filter: " + roleFilter.Team + "," + roleFilter.Environment + "," + roleFilter.Application + "," + roleFilter.Action + + if len(roleFilter.Team) > 0 && len(roleFilter.Environment) > 0 { + if roleFilter.AccessType == bean.APP_ACCESS_TYPE_HELM { + flag, err := impl.userAuthRepository.CreateDefaultHelmPolicies(roleFilter.Team, entityName, environment, tx) + if err != nil || flag == false { + return nil, err + } + } else { + flag, err := impl.userAuthRepository.CreateDefaultPolicies(roleFilter.Team, entityName, environment, tx) + if err != nil || flag == false { + return nil, err + } + } + roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "user", request) return nil, err } - } else { - flag, err := impl.userAuthRepository.CreateDefaultPolicies(roleFilter.Team, entityName, environment, tx) + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + request.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + continue + } + } else if len(roleFilter.Entity) > 0 { + flag, err := impl.userAuthRepository.CreateDefaultPoliciesForGlobalEntity(roleFilter.Entity, entityName, roleFilter.Action, tx) if err != nil || flag == false { return nil, err } - } - roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) - if err != nil { - impl.logger.Errorw("Error in fetching role by filter", "user", request) - return nil, err - } - if roleModel.Id == 0 { - impl.logger.Debugw("no role found for given filter", "filter", roleFilter) - request.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "user", request) + return nil, err + } + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + request.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + continue + } + } else { continue } - } else if len(roleFilter.Entity) > 0 { - flag, err := impl.userAuthRepository.CreateDefaultPoliciesForGlobalEntity(roleFilter.Entity, entityName, roleFilter.Action, tx) - if err != nil || flag == false { - return nil, err - } - roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + } + + if roleModel.Id > 0 { + roleGroupMappingModel := &repository2.RoleGroupRoleMapping{RoleGroupId: model.Id, RoleId: roleModel.Id} + roleGroupMappingModel.CreatedBy = request.UserId + roleGroupMappingModel.UpdatedBy = request.UserId + roleGroupMappingModel.CreatedOn = time.Now() + roleGroupMappingModel.UpdatedOn = time.Now() + roleGroupMappingModel, err = impl.roleGroupRepository.CreateRoleGroupRoleMapping(roleGroupMappingModel, tx) if err != nil { - impl.logger.Errorw("Error in fetching role by filter", "user", request) return nil, err } - if roleModel.Id == 0 { - impl.logger.Debugw("no role found for given filter", "filter", roleFilter) - request.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action - continue - } - } else { - continue + policies = append(policies, casbin2.Policy{Type: "g", Sub: casbin2.Subject(model.CasbinName), Obj: casbin2.Object(roleModel.Role)}) } } - - if roleModel.Id > 0 { - roleGroupMappingModel := &repository2.RoleGroupRoleMapping{RoleGroupId: model.Id, RoleId: roleModel.Id} - roleGroupMappingModel.CreatedBy = request.UserId - roleGroupMappingModel.UpdatedBy = request.UserId - roleGroupMappingModel.CreatedOn = time.Now() - roleGroupMappingModel.UpdatedOn = time.Now() - roleGroupMappingModel, err = impl.roleGroupRepository.CreateRoleGroupRoleMapping(roleGroupMappingModel, tx) - if err != nil { - return nil, err - } - policies = append(policies, casbin2.Policy{Type: "g", Sub: casbin2.Subject(model.CasbinName), Obj: casbin2.Object(roleModel.Role)}) - } } } } @@ -207,7 +214,95 @@ func (impl RoleGroupServiceImpl) CreateRoleGroup(request *bean.RoleGroup) (*bean return request, nil } -func (impl RoleGroupServiceImpl) UpdateRoleGroup(request *bean.RoleGroup, token string, managerAuth func(token string, object string) bool) (*bean.RoleGroup, error) { +func (impl RoleGroupServiceImpl) CreateOrUpdateRoleGroupForClusterEntity(roleFilter bean.RoleFilter, userId int32, + model *repository2.RoleGroup, existingRoles map[int]*repository2.RoleGroupRoleMapping, token string, + managerAuth func(resource, token string, object string) bool, tx *pg.Tx) ([]casbin2.Policy, error) { + var policiesToBeAdded []casbin2.Policy + if roleFilter.Namespace == "" { + roleFilter.Namespace = "NONE" + } + if roleFilter.Group == "" { + roleFilter.Group = "NONE" + } + if roleFilter.Kind == "" { + roleFilter.Kind = "NONE" + } + if roleFilter.Resource == "" { + roleFilter.Resource = "NONE" + } + namespaces := strings.Split(roleFilter.Namespace, ",") + groups := strings.Split(roleFilter.Group, ",") + kinds := strings.Split(roleFilter.Kind, ",") + resources := strings.Split(roleFilter.Resource, ",") + + for _, namespace := range namespaces { + for _, group := range groups { + for _, kind := range kinds { + for _, resource := range resources { + if namespace == "NONE" { + namespace = "" + } + if group == "NONE" { + group = "" + } + if kind == "NONE" { + kind = "" + } + if resource == "NONE" { + resource = "" + } + if managerAuth != nil { + isValidAuth := impl.userCommonService.CheckRbacForClusterEntity(roleFilter.Cluster, namespace, group, kind, resource, token, managerAuth) + if !isValidAuth { + continue + } + } + roleModel, err := impl.userAuthRepository.GetRoleByFilterForClusterEntity(roleFilter.Cluster, namespace, group, kind, resource, roleFilter.Action) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "err", err) + return policiesToBeAdded, err + } + if roleModel.Id == 0 { + flag, err := impl.userAuthRepository.CreateDefaultPoliciesForClusterEntity(roleFilter.Entity, roleFilter.Cluster, namespace, group, kind, resource, tx) + if err != nil || flag == false { + return policiesToBeAdded, err + } + roleModel, err = impl.userAuthRepository.GetRoleByFilterForClusterEntity(roleFilter.Cluster, namespace, group, kind, resource, roleFilter.Action) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "err", err) + return policiesToBeAdded, err + } + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + continue + } + } + if _, ok := existingRoles[roleModel.Id]; ok { + //Adding policies which are removed + policiesToBeAdded = append(policiesToBeAdded, casbin2.Policy{Type: "g", Sub: casbin2.Subject(model.CasbinName), Obj: casbin2.Object(roleModel.Role)}) + } else { + if roleModel.Id > 0 { + //new role ids in new array, add it + roleGroupMappingModel := &repository2.RoleGroupRoleMapping{RoleGroupId: model.Id, RoleId: roleModel.Id} + roleGroupMappingModel.CreatedBy = userId + roleGroupMappingModel.UpdatedBy = userId + roleGroupMappingModel.CreatedOn = time.Now() + roleGroupMappingModel.UpdatedOn = time.Now() + roleGroupMappingModel, err = impl.roleGroupRepository.CreateRoleGroupRoleMapping(roleGroupMappingModel, tx) + if err != nil { + return nil, err + } + policiesToBeAdded = append(policiesToBeAdded, casbin2.Policy{Type: "g", Sub: casbin2.Subject(model.CasbinName), Obj: casbin2.Object(roleModel.Role)}) + } + } + } + } + } + } + return policiesToBeAdded, nil +} + +func (impl RoleGroupServiceImpl) UpdateRoleGroup(request *bean.RoleGroup, token string, managerAuth func(resource, token string, object string) bool) (*bean.RoleGroup, error) { dbConnection := impl.roleGroupRepository.GetConnection() tx, err := dbConnection.Begin() if err != nil { @@ -260,98 +355,106 @@ func (impl RoleGroupServiceImpl) UpdateRoleGroup(request *bean.RoleGroup, token //Adding New Policies var policies []casbin2.Policy for _, roleFilter := range request.RoleFilters { - if len(roleFilter.Team) > 0 { - // check auth only for apps permission, skip for chart group - rbacObject := fmt.Sprintf("%s", strings.ToLower(roleFilter.Team)) - isValidAuth := managerAuth(token, rbacObject) - if !isValidAuth { - continue + if roleFilter.Entity == bean.CLUSTER_ENTITIY { + policiesToBeAdded, err := impl.CreateOrUpdateRoleGroupForClusterEntity(roleFilter, request.UserId, roleGroup, existingRoles, token, managerAuth, tx) + policies = append(policies, policiesToBeAdded...) + if err != nil { + impl.logger.Errorw("error in creating updating role group for cluster entity", "err", err, "roleFilter", roleFilter) } - } - - if roleFilter.EntityName == "" { - roleFilter.EntityName = "NONE" - } - if roleFilter.Environment == "" { - roleFilter.Environment = "NONE" - } - entityNames := strings.Split(roleFilter.EntityName, ",") - environments := strings.Split(roleFilter.Environment, ",") - for _, environment := range environments { - for _, entityName := range entityNames { - if entityName == "NONE" { - entityName = "" - } - if environment == "NONE" { - environment = "" - } - roleModel, err := impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) - if err != nil { - impl.logger.Errorw("Error in fetching role by filter", "user", request) - return nil, err + } else { + if len(roleFilter.Team) > 0 { + // check auth only for apps permission, skip for chart group + rbacObject := fmt.Sprintf("%s", strings.ToLower(roleFilter.Team)) + isValidAuth := managerAuth(casbin2.ResourceUser, token, rbacObject) + if !isValidAuth { + continue } - if roleModel.Id == 0 { - impl.logger.Debugw("no role found for given filter", "filter", roleFilter) - request.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + } - if len(roleFilter.Team) > 0 { - if roleFilter.AccessType == bean.APP_ACCESS_TYPE_HELM { - flag, err := impl.userAuthRepository.CreateDefaultHelmPolicies(roleFilter.Team, entityName, environment, tx) - if err != nil || flag == false { + if roleFilter.EntityName == "" { + roleFilter.EntityName = "NONE" + } + if roleFilter.Environment == "" { + roleFilter.Environment = "NONE" + } + entityNames := strings.Split(roleFilter.EntityName, ",") + environments := strings.Split(roleFilter.Environment, ",") + for _, environment := range environments { + for _, entityName := range entityNames { + if entityName == "NONE" { + entityName = "" + } + if environment == "NONE" { + environment = "" + } + roleModel, err := impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "user", request) + return nil, err + } + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + request.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + + if len(roleFilter.Team) > 0 { + if roleFilter.AccessType == bean.APP_ACCESS_TYPE_HELM { + flag, err := impl.userAuthRepository.CreateDefaultHelmPolicies(roleFilter.Team, entityName, environment, tx) + if err != nil || flag == false { + return nil, err + } + } else { + flag, err := impl.userAuthRepository.CreateDefaultPolicies(roleFilter.Team, entityName, environment, tx) + if err != nil || flag == false { + return nil, err + } + } + roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "user", request) return nil, err } - } else { - flag, err := impl.userAuthRepository.CreateDefaultPolicies(roleFilter.Team, entityName, environment, tx) + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + request.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + continue + } + } else if len(roleFilter.Entity) > 0 { + flag, err := impl.userAuthRepository.CreateDefaultPoliciesForGlobalEntity(roleFilter.Entity, entityName, roleFilter.Action, tx) if err != nil || flag == false { return nil, err } - } - roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) - if err != nil { - impl.logger.Errorw("Error in fetching role by filter", "user", request) - return nil, err - } - if roleModel.Id == 0 { - impl.logger.Debugw("no role found for given filter", "filter", roleFilter) - request.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action - continue - } - } else if len(roleFilter.Entity) > 0 { - flag, err := impl.userAuthRepository.CreateDefaultPoliciesForGlobalEntity(roleFilter.Entity, entityName, roleFilter.Action, tx) - if err != nil || flag == false { - return nil, err - } - roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) - if err != nil { - impl.logger.Errorw("Error in fetching role by filter", "user", request) - return nil, err - } - if roleModel.Id == 0 { - impl.logger.Debugw("no role found for given filter", "filter", roleFilter) - request.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "user", request) + return nil, err + } + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + request.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + continue + } + } else { continue } - } else { - continue } - } - if _, ok := existingRoles[roleModel.Id]; ok { - //Adding policies which is removed - policies = append(policies, casbin2.Policy{Type: "g", Sub: casbin2.Subject(roleGroup.CasbinName), Obj: casbin2.Object(roleModel.Role)}) - } else { - if roleModel.Id > 0 { - //new role ids in new array, add it - roleGroupMappingModel := &repository2.RoleGroupRoleMapping{RoleGroupId: request.Id, RoleId: roleModel.Id} - roleGroupMappingModel.CreatedBy = request.UserId - roleGroupMappingModel.UpdatedBy = request.UserId - roleGroupMappingModel.CreatedOn = time.Now() - roleGroupMappingModel.UpdatedOn = time.Now() - roleGroupMappingModel, err = impl.roleGroupRepository.CreateRoleGroupRoleMapping(roleGroupMappingModel, tx) - if err != nil { - return nil, err - } + if _, ok := existingRoles[roleModel.Id]; ok { + //Adding policies which is removed policies = append(policies, casbin2.Policy{Type: "g", Sub: casbin2.Subject(roleGroup.CasbinName), Obj: casbin2.Object(roleModel.Role)}) + } else { + if roleModel.Id > 0 { + //new role ids in new array, add it + roleGroupMappingModel := &repository2.RoleGroupRoleMapping{RoleGroupId: request.Id, RoleId: roleModel.Id} + roleGroupMappingModel.CreatedBy = request.UserId + roleGroupMappingModel.UpdatedBy = request.UserId + roleGroupMappingModel.CreatedOn = time.Now() + roleGroupMappingModel.UpdatedOn = time.Now() + roleGroupMappingModel, err = impl.roleGroupRepository.CreateRoleGroupRoleMapping(roleGroupMappingModel, tx) + if err != nil { + return nil, err + } + policies = append(policies, casbin2.Policy{Type: "g", Sub: casbin2.Subject(roleGroup.CasbinName), Obj: casbin2.Object(roleModel.Role)}) + } } } } @@ -371,7 +474,13 @@ func (impl RoleGroupServiceImpl) UpdateRoleGroup(request *bean.RoleGroup, token return request, nil } -const AllEnvironment string = "" +const ( + AllEnvironment string = "" + AllNamespace string = "" + AllGroup string = "" + AllKind string = "" + AllResource string = "" +) func (impl RoleGroupServiceImpl) FetchRoleGroupsById(id int32) (*bean.RoleGroup, error) { roleGroup, err := impl.roleGroupRepository.GetRoleGroupById(id) @@ -402,18 +511,50 @@ func (impl RoleGroupServiceImpl) getRoleGroupMetadata(roleGroup *repository2.Rol if len(role.Team) > 0 { key = fmt.Sprintf("%s_%s_%s", role.Team, role.Action, role.AccessType) } else if len(role.Entity) > 0 { - key = fmt.Sprintf("%s_%s", role.Entity, role.Action) + if role.Entity == bean.CLUSTER_ENTITIY { + key = fmt.Sprintf("%s_%s_%s_%s_%s_%s", role.Entity, role.Action, role.Cluster, + role.Namespace, role.Group, role.Kind) + } else { + key = fmt.Sprintf("%s_%s", role.Entity, role.Action) + } } if _, ok := roleFilterMap[key]; ok { - envArr := strings.Split(roleFilterMap[key].Environment, ",") - if containsArr(envArr, AllEnvironment) { - roleFilterMap[key].Environment = AllEnvironment - } else if !containsArr(envArr, role.Environment) { - roleFilterMap[key].Environment = fmt.Sprintf("%s,%s", roleFilterMap[key].Environment, role.Environment) - } - entityArr := strings.Split(roleFilterMap[key].EntityName, ",") - if !containsArr(entityArr, role.EntityName) { - roleFilterMap[key].EntityName = fmt.Sprintf("%s,%s", roleFilterMap[key].EntityName, role.EntityName) + if role.Entity == bean.CLUSTER_ENTITIY { + namespaceArr := strings.Split(roleFilterMap[key].Namespace, ",") + if containsArr(namespaceArr, AllNamespace) { + roleFilterMap[key].Namespace = AllNamespace + } else if !containsArr(namespaceArr, role.Namespace) { + roleFilterMap[key].Namespace = fmt.Sprintf("%s,%s", roleFilterMap[key].Namespace, role.Namespace) + } + groupArr := strings.Split(roleFilterMap[key].Group, ",") + if containsArr(groupArr, AllGroup) { + roleFilterMap[key].Group = AllGroup + } else if !containsArr(groupArr, role.Group) { + roleFilterMap[key].Group = fmt.Sprintf("%s,%s", roleFilterMap[key].Group, role.Group) + } + kindArr := strings.Split(roleFilterMap[key].Kind, ",") + if containsArr(kindArr, AllKind) { + roleFilterMap[key].Kind = AllKind + } else if !containsArr(kindArr, role.Kind) { + roleFilterMap[key].Kind = fmt.Sprintf("%s,%s", roleFilterMap[key].Kind, role.Kind) + } + resourceArr := strings.Split(roleFilterMap[key].Resource, ",") + if containsArr(resourceArr, AllResource) { + roleFilterMap[key].Resource = AllResource + } else if !containsArr(resourceArr, role.Resource) { + roleFilterMap[key].Resource = fmt.Sprintf("%s,%s", roleFilterMap[key].Resource, role.Resource) + } + } else { + envArr := strings.Split(roleFilterMap[key].Environment, ",") + if containsArr(envArr, AllEnvironment) { + roleFilterMap[key].Environment = AllEnvironment + } else if !containsArr(envArr, role.Environment) { + roleFilterMap[key].Environment = fmt.Sprintf("%s,%s", roleFilterMap[key].Environment, role.Environment) + } + entityArr := strings.Split(roleFilterMap[key].EntityName, ",") + if !containsArr(entityArr, role.EntityName) { + roleFilterMap[key].EntityName = fmt.Sprintf("%s,%s", roleFilterMap[key].EntityName, role.EntityName) + } } } else { roleFilterMap[key] = &bean.RoleFilter{ @@ -423,6 +564,11 @@ func (impl RoleGroupServiceImpl) getRoleGroupMetadata(roleGroup *repository2.Rol EntityName: role.EntityName, Action: role.Action, AccessType: role.AccessType, + Cluster: role.Cluster, + Namespace: role.Namespace, + Group: role.Group, + Kind: role.Kind, + Resource: role.Resource, } } } @@ -569,6 +715,11 @@ func (impl RoleGroupServiceImpl) FetchRolesForGroups(groupNames []string) ([]*be Environment: role.Environment, Team: role.Team, AccessType: role.AccessType, + Cluster: role.Cluster, + Namespace: role.Namespace, + Group: role.Group, + Kind: role.Kind, + Resource: role.Resource, } list = append(list, bean) } diff --git a/pkg/user/UserAuthService.go b/pkg/user/UserAuthService.go index f1ed516c4e..c8ff66a96a 100644 --- a/pkg/user/UserAuthService.go +++ b/pkg/user/UserAuthService.go @@ -435,6 +435,11 @@ func (impl UserAuthServiceImpl) CreateRole(roleData *bean.RoleData) (bool, error EntityName: roleData.EntityName, Environment: roleData.Environment, Action: roleData.Action, + Cluster: roleData.Cluster, + Namespace: roleData.Namespace, + Group: roleData.Group, + Kind: roleData.Kind, + Resource: roleData.Resource, } dbConnection := impl.userRepository.GetConnection() tx, err := dbConnection.Begin() diff --git a/pkg/user/UserCommonService.go b/pkg/user/UserCommonService.go index 4d287777b9..471ec39f1e 100644 --- a/pkg/user/UserCommonService.go +++ b/pkg/user/UserCommonService.go @@ -13,8 +13,9 @@ import ( ) type UserCommonService interface { - RemoveRolesAndReturnEliminatedPolicies(userInfo *bean.UserInfo, existingRoleIds map[int]repository2.UserRoleModel, eliminatedRoleIds map[int]*repository2.UserRoleModel, tx *pg.Tx, token string, managerAuth func(token string, object string) bool) ([]casbin2.Policy, error) - RemoveRolesAndReturnEliminatedPoliciesForGroups(request *bean.RoleGroup, existingRoles map[int]*repository2.RoleGroupRoleMapping, eliminatedRoles map[int]*repository2.RoleGroupRoleMapping, tx *pg.Tx, token string, managerAuth func(token string, object string) bool) ([]casbin2.Policy, error) + RemoveRolesAndReturnEliminatedPolicies(userInfo *bean.UserInfo, existingRoleIds map[int]repository2.UserRoleModel, eliminatedRoleIds map[int]*repository2.UserRoleModel, tx *pg.Tx, token string, managerAuth func(resource, token, object string) bool) ([]casbin2.Policy, error) + RemoveRolesAndReturnEliminatedPoliciesForGroups(request *bean.RoleGroup, existingRoles map[int]*repository2.RoleGroupRoleMapping, eliminatedRoles map[int]*repository2.RoleGroupRoleMapping, tx *pg.Tx, token string, managerAuth func(resource string, token string, object string) bool) ([]casbin2.Policy, error) + CheckRbacForClusterEntity(cluster, namespace, group, kind, resource, token string, managerAuth func(resource, token, object string) bool) bool } type UserCommonServiceImpl struct { @@ -41,52 +42,110 @@ func NewUserCommonServiceImpl(userAuthRepository repository2.UserAuthRepository, return serviceImpl } -func (impl UserCommonServiceImpl) RemoveRolesAndReturnEliminatedPolicies(userInfo *bean.UserInfo, existingRoleIds map[int]repository2.UserRoleModel, eliminatedRoleIds map[int]*repository2.UserRoleModel, tx *pg.Tx, token string, managerAuth func(token string, object string) bool) ([]casbin2.Policy, error) { +func (impl UserCommonServiceImpl) RemoveRolesAndReturnEliminatedPolicies(userInfo *bean.UserInfo, + existingRoleIds map[int]repository2.UserRoleModel, eliminatedRoleIds map[int]*repository2.UserRoleModel, + tx *pg.Tx, token string, managerAuth func(resource, token, object string) bool) ([]casbin2.Policy, error) { var eliminatedPolicies []casbin2.Policy // DELETE Removed Items for _, roleFilter := range userInfo.RoleFilters { - if len(roleFilter.Team) > 0 { // check auth only for apps permission, skip for chart group - rbacObject := fmt.Sprintf("%s", strings.ToLower(roleFilter.Team)) - isValidAuth := managerAuth(token, rbacObject) - if !isValidAuth { - continue + if roleFilter.Entity == bean.CLUSTER_ENTITIY { + if roleFilter.Namespace == "" { + roleFilter.Namespace = "NONE" } - } + if roleFilter.Group == "" { + roleFilter.Group = "NONE" + } + if roleFilter.Kind == "" { + roleFilter.Kind = "NONE" + } + if roleFilter.Resource == "" { + roleFilter.Resource = "NONE" + } + namespaces := strings.Split(roleFilter.Namespace, ",") + groups := strings.Split(roleFilter.Group, ",") + kinds := strings.Split(roleFilter.Kind, ",") + resources := strings.Split(roleFilter.Resource, ",") - if roleFilter.EntityName == "" { - roleFilter.EntityName = "NONE" - } - if roleFilter.Environment == "" { - roleFilter.Environment = "NONE" - } - entityNames := strings.Split(roleFilter.EntityName, ",") - environments := strings.Split(roleFilter.Environment, ",") - for _, environment := range environments { - for _, entityName := range entityNames { - if entityName == "NONE" { - entityName = "" - } - if environment == "NONE" { - environment = "" - } - roleModel, err := impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) - if err != nil { - impl.logger.Errorw("Error in fetching roles by filter", "user", userInfo) - return nil, err + for _, namespace := range namespaces { + for _, group := range groups { + for _, kind := range kinds { + for _, resource := range resources { + if namespace == "NONE" { + namespace = "" + } + if group == "NONE" { + group = "" + } + if kind == "NONE" { + kind = "" + } + if resource == "NONE" { + resource = "" + } + isValidAuth := impl.CheckRbacForClusterEntity(roleFilter.Cluster, namespace, group, kind, resource, token, managerAuth) + if !isValidAuth { + continue + } + roleModel, err := impl.userAuthRepository.GetRoleByFilterForClusterEntity(roleFilter.Cluster, namespace, group, kind, resource, roleFilter.Action) + if err != nil { + impl.logger.Errorw("Error in fetching roles by filter", "roleFilter", roleFilter) + return nil, err + } + if roleModel.Id == 0 { + impl.logger.Warnw("no role found for given filter", "filter", roleFilter) + continue + } + if _, ok := existingRoleIds[roleModel.Id]; ok { + delete(existingRoleIds, roleModel.Id) + } + } + } } - if roleModel.Id == 0 { - impl.logger.Debugw("no role found for given filter", "filter", roleFilter) - userInfo.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + } + } else { + if len(roleFilter.Team) > 0 { // check auth only for apps permission, skip for chart group + rbacObject := fmt.Sprintf("%s", strings.ToLower(roleFilter.Team)) + isValidAuth := managerAuth(casbin2.ResourceUser, token, rbacObject) + if !isValidAuth { continue } - if _, ok := existingRoleIds[roleModel.Id]; ok { - delete(eliminatedRoleIds, roleModel.Id) + } + + if roleFilter.EntityName == "" { + roleFilter.EntityName = "NONE" + } + if roleFilter.Environment == "" { + roleFilter.Environment = "NONE" + } + entityNames := strings.Split(roleFilter.EntityName, ",") + environments := strings.Split(roleFilter.Environment, ",") + for _, environment := range environments { + for _, entityName := range entityNames { + if entityName == "NONE" { + entityName = "" + } + if environment == "NONE" { + environment = "" + } + roleModel, err := impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + if err != nil { + impl.logger.Errorw("Error in fetching roles by filter", "user", userInfo) + return nil, err + } + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + userInfo.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + continue + } + if _, ok := existingRoleIds[roleModel.Id]; ok { + delete(eliminatedRoleIds, roleModel.Id) + } } } } } - //delete remaining Ids from casbin role mapping table in orchestrator and casbin policy db + // delete remaining Ids from casbin role mapping table in orchestrator and casbin policy db // which are existing but not provided in this request for _, userRoleModel := range eliminatedRoleIds { @@ -96,7 +155,13 @@ func (impl UserCommonServiceImpl) RemoveRolesAndReturnEliminatedPolicies(userInf } if len(role.Team) > 0 { rbacObject := fmt.Sprintf("%s", strings.ToLower(role.Team)) - isValidAuth := managerAuth(token, rbacObject) + isValidAuth := managerAuth(casbin2.ResourceUser, token, rbacObject) + if !isValidAuth { + continue + } + } + if role.Entity == bean.CLUSTER_ENTITIY { + isValidAuth := impl.CheckRbacForClusterEntity(role.Cluster, role.Namespace, role.Group, role.Kind, role.Resource, token, managerAuth) if !isValidAuth { continue } @@ -112,47 +177,103 @@ func (impl UserCommonServiceImpl) RemoveRolesAndReturnEliminatedPolicies(userInf return eliminatedPolicies, nil } -func (impl UserCommonServiceImpl) RemoveRolesAndReturnEliminatedPoliciesForGroups(request *bean.RoleGroup, existingRoles map[int]*repository2.RoleGroupRoleMapping, eliminatedRoles map[int]*repository2.RoleGroupRoleMapping, tx *pg.Tx, token string, managerAuth func(token string, object string) bool) ([]casbin2.Policy, error) { +func (impl UserCommonServiceImpl) RemoveRolesAndReturnEliminatedPoliciesForGroups(request *bean.RoleGroup, existingRoles map[int]*repository2.RoleGroupRoleMapping, eliminatedRoles map[int]*repository2.RoleGroupRoleMapping, tx *pg.Tx, token string, managerAuth func(resource string, token string, object string) bool) ([]casbin2.Policy, error) { // Filter out removed items in current request //var policies []casbin2.Policy for _, roleFilter := range request.RoleFilters { - if len(roleFilter.Team) > 0 { // check auth only for apps permission, skip for chart group - rbacObject := fmt.Sprintf("%s", strings.ToLower(roleFilter.Team)) - isValidAuth := managerAuth(token, rbacObject) - if !isValidAuth { - continue + if roleFilter.Entity == bean.CLUSTER_ENTITIY { + if roleFilter.Namespace == "" { + roleFilter.Namespace = "NONE" } - } + if roleFilter.Group == "" { + roleFilter.Group = "NONE" + } + if roleFilter.Kind == "" { + roleFilter.Kind = "NONE" + } + if roleFilter.Resource == "" { + roleFilter.Resource = "NONE" + } + namespaces := strings.Split(roleFilter.Namespace, ",") + groups := strings.Split(roleFilter.Group, ",") + kinds := strings.Split(roleFilter.Kind, ",") + resources := strings.Split(roleFilter.Resource, ",") - if roleFilter.EntityName == "" { - roleFilter.EntityName = "NONE" - } - if roleFilter.Environment == "" { - roleFilter.Environment = "NONE" - } - entityNames := strings.Split(roleFilter.EntityName, ",") - environments := strings.Split(roleFilter.Environment, ",") - for _, environment := range environments { - for _, entityName := range entityNames { - if entityName == "NONE" { - entityName = "" - } - if environment == "NONE" { - environment = "" - } - roleModel, err := impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) - if err != nil { - impl.logger.Errorw("Error in fetching roles by filter", "user", request) - return nil, err + for _, namespace := range namespaces { + for _, group := range groups { + for _, kind := range kinds { + for _, resource := range resources { + if namespace == "NONE" { + namespace = "" + } + if group == "NONE" { + group = "" + } + if kind == "NONE" { + kind = "" + } + if resource == "NONE" { + resource = "" + } + isValidAuth := impl.CheckRbacForClusterEntity(roleFilter.Cluster, namespace, group, kind, resource, token, managerAuth) + if !isValidAuth { + continue + } + roleModel, err := impl.userAuthRepository.GetRoleByFilterForClusterEntity(roleFilter.Cluster, namespace, group, kind, resource, roleFilter.Action) + if err != nil { + impl.logger.Errorw("Error in fetching roles by filter", "user", request) + return nil, err + } + if roleModel.Id == 0 { + impl.logger.Warnw("no role found for given filter", "filter", roleFilter) + continue + } + if _, ok := existingRoles[roleModel.Id]; ok { + delete(eliminatedRoles, roleModel.Id) + } + } + } } - if roleModel.Id == 0 { - impl.logger.Warnw("no role found for given filter", "filter", roleFilter) - request.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + } + } else { + if len(roleFilter.Team) > 0 { // check auth only for apps permission, skip for chart group + rbacObject := fmt.Sprintf("%s", strings.ToLower(roleFilter.Team)) + isValidAuth := managerAuth(casbin2.ResourceUser, token, rbacObject) + if !isValidAuth { continue } - //roleModel := roleModels[0] - if _, ok := existingRoles[roleModel.Id]; ok { - delete(eliminatedRoles, roleModel.Id) + } + + if roleFilter.EntityName == "" { + roleFilter.EntityName = "NONE" + } + if roleFilter.Environment == "" { + roleFilter.Environment = "NONE" + } + entityNames := strings.Split(roleFilter.EntityName, ",") + environments := strings.Split(roleFilter.Environment, ",") + for _, environment := range environments { + for _, entityName := range entityNames { + if entityName == "NONE" { + entityName = "" + } + if environment == "NONE" { + environment = "" + } + roleModel, err := impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + if err != nil { + impl.logger.Errorw("Error in fetching roles by filter", "user", request) + return nil, err + } + if roleModel.Id == 0 { + impl.logger.Warnw("no role found for given filter", "filter", roleFilter) + request.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + continue + } + //roleModel := roleModels[0] + if _, ok := existingRoles[roleModel.Id]; ok { + delete(eliminatedRoles, roleModel.Id) + } } } } @@ -168,7 +289,13 @@ func (impl UserCommonServiceImpl) RemoveRolesAndReturnEliminatedPoliciesForGroup } if len(role.Team) > 0 { rbacObject := fmt.Sprintf("%s", strings.ToLower(role.Team)) - isValidAuth := managerAuth(token, rbacObject) + isValidAuth := managerAuth(casbin2.ResourceUser, token, rbacObject) + if !isValidAuth { + continue + } + } + if role.Entity == bean.CLUSTER_ENTITIY { + isValidAuth := impl.CheckRbacForClusterEntity(role.Cluster, role.Namespace, role.Group, role.Kind, role.Resource, token, managerAuth) if !isValidAuth { continue } @@ -194,3 +321,45 @@ func containsArr(s []string, e string) bool { } return false } + +func (impl UserCommonServiceImpl) CheckRbacForClusterEntity(cluster, namespace, group, kind, resource, token string, managerAuth func(resource, token, object string) bool) bool { + if namespace == "NONE" { + namespace = "" + } + if group == "NONE" { + group = "" + } + if kind == "NONE" { + kind = "" + } + if resource == "NONE" { + resource = "" + } + namespaceObj := namespace + groupObj := group + kindObj := kind + resourceObj := resource + if namespace == "" { + namespaceObj = "*" + } + if group == "" { + groupObj = "*" + } + if kind == "" { + kindObj = "*" + } + if resource == "" { + resourceObj = "*" + } + + rbacResource := fmt.Sprintf("%s/%s/%s", strings.ToLower(cluster), strings.ToLower(namespaceObj), casbin2.ResourceUser) + resourcesArray := strings.Split(resourceObj, ",") + for _, resourceVal := range resourcesArray { + rbacObject := fmt.Sprintf("%s/%s/%s", strings.ToLower(groupObj), strings.ToLower(kindObj), strings.ToLower(resourceVal)) + allowed := managerAuth(rbacResource, token, rbacObject) + if !allowed { + return false + } + } + return true +} diff --git a/pkg/user/UserService.go b/pkg/user/UserService.go index 2bb764fc72..9e1a00161c 100644 --- a/pkg/user/UserService.go +++ b/pkg/user/UserService.go @@ -24,6 +24,7 @@ import ( "github.com/devtron-labs/devtron/api/bean" "github.com/devtron-labs/devtron/internal/constants" "github.com/devtron-labs/devtron/internal/util" + "github.com/devtron-labs/devtron/pkg/sql" casbin2 "github.com/devtron-labs/devtron/pkg/user/casbin" repository2 "github.com/devtron-labs/devtron/pkg/user/repository" util2 "github.com/devtron-labs/devtron/util" @@ -36,9 +37,9 @@ import ( ) type UserService interface { - CreateUser(userInfo *bean.UserInfo, token string, managerAuth func(token string, object string) bool) ([]*bean.UserInfo, error) + CreateUser(userInfo *bean.UserInfo, token string, managerAuth func(resource, token string, object string) bool) ([]*bean.UserInfo, error) SelfRegisterUserIfNotExists(userInfo *bean.UserInfo) ([]*bean.UserInfo, error) - UpdateUser(userInfo *bean.UserInfo, token string, managerAuth func(token string, object string) bool) (*bean.UserInfo, bool, bool, []string, error) + 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) @@ -94,7 +95,7 @@ func (impl UserServiceImpl) validateUserRequest(userInfo *bean.UserInfo) (bool, for _, roleFilter := range userInfo.RoleFilters { if len(roleFilter.Team) > 0 && len(roleFilter.Action) > 0 { // - } else if len(roleFilter.Entity) > 0 { + } else if len(roleFilter.Entity) > 0 { //this will pass roleFilter for clusterEntity as well as chart-group // } else { invalid = true @@ -216,7 +217,7 @@ func (impl UserServiceImpl) saveUser(userInfo *bean.UserInfo, emailId string) (* return userInfo, nil } -func (impl UserServiceImpl) CreateUser(userInfo *bean.UserInfo, token string, managerAuth func(token string, object string) bool) ([]*bean.UserInfo, error) { +func (impl UserServiceImpl) CreateUser(userInfo *bean.UserInfo, token string, managerAuth func(resource, token string, object string) bool) ([]*bean.UserInfo, error) { var pass []string var userResponse []*bean.UserInfo emailIds := strings.Split(userInfo.EmailId, ",") @@ -238,7 +239,7 @@ func (impl UserServiceImpl) CreateUser(userInfo *bean.UserInfo, token string, ma // if not found, create new user if err == pg.ErrNoRows { - userInfo, err = impl.createUserIfNotExists(userInfo, emailId) + userInfo, err = impl.createUserIfNotExists(userInfo, emailId, token, managerAuth) if err != nil { impl.logger.Errorw("error while create user if not exists in db", "error", err) return nil, err @@ -255,7 +256,7 @@ func (impl UserServiceImpl) CreateUser(userInfo *bean.UserInfo, token string, ma } func (impl UserServiceImpl) updateUserIfExists(userInfo *bean.UserInfo, dbUser *repository2.UserModel, emailId string, - token string, managerAuth func(token string, object string) bool) (*bean.UserInfo, error) { + token string, managerAuth func(resource, token, object string) bool) (*bean.UserInfo, error) { updateUserInfo, err := impl.GetById(dbUser.Id) if err != nil && err != pg.ErrNoRows { impl.logger.Errorw("error while fetching user from db", "error", err) @@ -278,7 +279,7 @@ func (impl UserServiceImpl) updateUserIfExists(userInfo *bean.UserInfo, dbUser * return userInfo, nil } -func (impl UserServiceImpl) createUserIfNotExists(userInfo *bean.UserInfo, emailId string) (*bean.UserInfo, error) { +func (impl UserServiceImpl) createUserIfNotExists(userInfo *bean.UserInfo, emailId string, token string, managerAuth func(resource string, token string, object string) bool) (*bean.UserInfo, error) { // if not found, create new user dbConnection := impl.userRepository.GetConnection() tx, err := dbConnection.Begin() @@ -321,81 +322,89 @@ func (impl UserServiceImpl) createUserIfNotExists(userInfo *bean.UserInfo, email var policies []casbin2.Policy if userInfo.SuperAdmin == false { for _, roleFilter := range userInfo.RoleFilters { - - if roleFilter.EntityName == "" { - roleFilter.EntityName = "NONE" - } - if roleFilter.Environment == "" { - roleFilter.Environment = "NONE" - } - entityNames := strings.Split(roleFilter.EntityName, ",") - environments := strings.Split(roleFilter.Environment, ",") - for _, environment := range environments { - for _, entityName := range entityNames { - if entityName == "NONE" { - entityName = "" - } - if environment == "NONE" { - environment = "" - } - roleModel, err := impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) - if err != nil { - impl.logger.Errorw("Error in fetching role by filter", "user", userInfo) - return nil, err - } - if roleModel.Id == 0 { - impl.logger.Debugw("no role found for given filter", "filter", roleFilter) - //userInfo.Status = "role not fount for any given filter: " + roleFilter.Team + "," + roleFilter.Environment + "," + roleFilter.Application + "," + roleFilter.Action - - if len(roleFilter.Team) > 0 { - if roleFilter.AccessType == bean.APP_ACCESS_TYPE_HELM { - flag, err := impl.userAuthRepository.CreateDefaultHelmPolicies(roleFilter.Team, entityName, environment, tx) - if err != nil || flag == false { + if roleFilter.Entity == bean.CLUSTER_ENTITIY { + policiesToBeAdded, _, err := impl.CreateOrUpdateUserRolesForClusterEntity(roleFilter, userInfo.UserId, model, nil, token, managerAuth, tx) + if err != nil { + impl.logger.Errorw("error in creating user roles for clusterEntity", "err", err) + return nil, err + } + policies = append(policies, policiesToBeAdded...) + } else { + if roleFilter.EntityName == "" { + roleFilter.EntityName = "NONE" + } + if roleFilter.Environment == "" { + roleFilter.Environment = "NONE" + } + entityNames := strings.Split(roleFilter.EntityName, ",") + environments := strings.Split(roleFilter.Environment, ",") + for _, environment := range environments { + for _, entityName := range entityNames { + if entityName == "NONE" { + entityName = "" + } + if environment == "NONE" { + environment = "" + } + roleModel, err := impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "user", userInfo) + return nil, err + } + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + //userInfo.Status = "role not fount for any given filter: " + roleFilter.Team + "," + roleFilter.Environment + "," + roleFilter.Application + "," + roleFilter.Action + + if len(roleFilter.Team) > 0 { + if roleFilter.AccessType == bean.APP_ACCESS_TYPE_HELM { + flag, err := impl.userAuthRepository.CreateDefaultHelmPolicies(roleFilter.Team, entityName, environment, tx) + if err != nil || flag == false { + return nil, err + } + } else { + flag, err := impl.userAuthRepository.CreateDefaultPolicies(roleFilter.Team, entityName, environment, tx) + if err != nil || flag == false { + return nil, err + } + } + roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "user", userInfo) return nil, err } - } else { - flag, err := impl.userAuthRepository.CreateDefaultPolicies(roleFilter.Team, entityName, environment, tx) + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + userInfo.Status = "role not found for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + continue + } + } else if len(roleFilter.Entity) > 0 && roleFilter.Entity == "chart-group" { + flag, err := impl.userAuthRepository.CreateDefaultPoliciesForGlobalEntity(roleFilter.Entity, entityName, roleFilter.Action, tx) if err != nil || flag == false { return nil, err } - } - roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) - if err != nil { - impl.logger.Errorw("Error in fetching role by filter", "user", userInfo) - return nil, err - } - if roleModel.Id == 0 { - impl.logger.Debugw("no role found for given filter", "filter", roleFilter) - userInfo.Status = "role not found for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "user", userInfo) + return nil, err + } + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + userInfo.Status = "role not found for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + continue + } + } else { continue } - } else if len(roleFilter.Entity) > 0 && roleFilter.Entity == "chart-group" { - flag, err := impl.userAuthRepository.CreateDefaultPoliciesForGlobalEntity(roleFilter.Entity, entityName, roleFilter.Action, tx) - if err != nil || flag == false { - return nil, err - } - roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + } + //roleModel := roleModels[0] + if roleModel.Id > 0 { + userRoleModel := &repository2.UserRoleModel{UserId: model.Id, RoleId: roleModel.Id} + userRoleModel, err = impl.userAuthRepository.CreateUserRoleMapping(userRoleModel, tx) if err != nil { - impl.logger.Errorw("Error in fetching role by filter", "user", userInfo) return nil, err } - if roleModel.Id == 0 { - impl.logger.Debugw("no role found for given filter", "filter", roleFilter) - userInfo.Status = "role not found for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action - continue - } - } else { - continue - } - } - //roleModel := roleModels[0] - if roleModel.Id > 0 { - userRoleModel := &repository2.UserRoleModel{UserId: model.Id, RoleId: roleModel.Id} - userRoleModel, err = impl.userAuthRepository.CreateUserRoleMapping(userRoleModel, tx) - if err != nil { - return nil, err + policies = append(policies, casbin2.Policy{Type: "g", Sub: casbin2.Subject(model.EmailId), Obj: casbin2.Object(roleModel.Role)}) } - policies = append(policies, casbin2.Policy{Type: "g", Sub: casbin2.Subject(model.EmailId), Obj: casbin2.Object(roleModel.Role)}) } } } @@ -453,6 +462,99 @@ func (impl UserServiceImpl) createUserIfNotExists(userInfo *bean.UserInfo, email return userInfo, nil } +func (impl UserServiceImpl) CreateOrUpdateUserRolesForClusterEntity(roleFilter bean.RoleFilter, userId int32, + model *repository2.UserModel, existingRoles map[int]repository2.UserRoleModel, token string, + managerAuth func(resource, token string, object string) bool, tx *pg.Tx) ([]casbin2.Policy, bool, error) { + var policiesToBeAdded []casbin2.Policy + if roleFilter.Namespace == "" { + roleFilter.Namespace = "NONE" + } + if roleFilter.Group == "" { + roleFilter.Group = "NONE" + } + if roleFilter.Kind == "" { + roleFilter.Kind = "NONE" + } + if roleFilter.Resource == "" { + roleFilter.Resource = "NONE" + } + namespaces := strings.Split(roleFilter.Namespace, ",") + groups := strings.Split(roleFilter.Group, ",") + kinds := strings.Split(roleFilter.Kind, ",") + resources := strings.Split(roleFilter.Resource, ",") + + rolesChanged := false + for _, namespace := range namespaces { + for _, group := range groups { + for _, kind := range kinds { + for _, resource := range resources { + if namespace == "NONE" { + namespace = "" + } + if group == "NONE" { + group = "" + } + if kind == "NONE" { + kind = "" + } + if resource == "NONE" { + resource = "" + } + if managerAuth != nil { + isValidAuth := impl.userCommonService.CheckRbacForClusterEntity(roleFilter.Cluster, namespace, group, kind, resource, token, managerAuth) + if !isValidAuth { + continue + } + } + roleModel, err := impl.userAuthRepository.GetRoleByFilterForClusterEntity(roleFilter.Cluster, namespace, group, kind, resource, roleFilter.Action) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "err", err) + return policiesToBeAdded, rolesChanged, err + } + if roleModel.Id == 0 { + flag, err := impl.userAuthRepository.CreateDefaultPoliciesForClusterEntity(roleFilter.Entity, roleFilter.Cluster, namespace, group, kind, resource, tx) + if err != nil || flag == false { + return policiesToBeAdded, rolesChanged, err + } + roleModel, err = impl.userAuthRepository.GetRoleByFilterForClusterEntity(roleFilter.Cluster, namespace, group, kind, resource, roleFilter.Action) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "err", err) + return policiesToBeAdded, rolesChanged, err + } + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + continue + } + } + if _, ok := existingRoles[roleModel.Id]; ok { + //Adding policies which are removed + policiesToBeAdded = append(policiesToBeAdded, casbin2.Policy{Type: "g", Sub: casbin2.Subject(model.EmailId), Obj: casbin2.Object(roleModel.Role)}) + } else { + if roleModel.Id > 0 { + rolesChanged = true + userRoleModel := &repository2.UserRoleModel{ + UserId: model.Id, + RoleId: roleModel.Id, + AuditLog: sql.AuditLog{ + CreatedBy: userId, + CreatedOn: time.Now(), + UpdatedBy: userId, + UpdatedOn: time.Now(), + }} + userRoleModel, err = impl.userAuthRepository.CreateUserRoleMapping(userRoleModel, tx) + if err != nil { + return nil, rolesChanged, err + } + policiesToBeAdded = append(policiesToBeAdded, casbin2.Policy{Type: "g", Sub: casbin2.Subject(model.EmailId), Obj: casbin2.Object(roleModel.Role)}) + } + } + } + } + } + } + return policiesToBeAdded, rolesChanged, nil +} + func (impl UserServiceImpl) mergeRoleFilter(oldR []bean.RoleFilter, newR []bean.RoleFilter) []bean.RoleFilter { var roleFilters []bean.RoleFilter keysMap := make(map[string]bool) @@ -464,12 +566,19 @@ func (impl UserServiceImpl) mergeRoleFilter(oldR []bean.RoleFilter, newR []bean. EntityName: role.EntityName, Action: role.Action, AccessType: role.AccessType, + Cluster: role.Cluster, + Namespace: role.Namespace, + Group: role.Group, + Kind: role.Kind, + Resource: role.Resource, }) - key := fmt.Sprintf("%s-%s-%s-%s-%s-%s", role.Entity, role.Team, role.Environment, role.EntityName, role.Action, role.AccessType) + key := fmt.Sprintf("%s-%s-%s-%s-%s-%s-%s-%s-%s-%s-%s", role.Entity, role.Team, role.Environment, + role.EntityName, role.Action, role.AccessType, role.Cluster, role.Namespace, role.Group, role.Kind, role.Resource) keysMap[key] = true } for _, role := range newR { - key := fmt.Sprintf("%s-%s-%s-%s-%s-%s", role.Entity, role.Team, role.Environment, role.EntityName, role.Action, role.AccessType) + key := fmt.Sprintf("%s-%s-%s-%s-%s-%s-%s-%s-%s-%s-%s", role.Entity, role.Team, role.Environment, + role.EntityName, role.Action, role.AccessType, role.Cluster, role.Namespace, role.Group, role.Kind, role.Resource) if _, ok := keysMap[key]; !ok { roleFilters = append(roleFilters, bean.RoleFilter{ Entity: role.Entity, @@ -478,6 +587,11 @@ func (impl UserServiceImpl) mergeRoleFilter(oldR []bean.RoleFilter, newR []bean. EntityName: role.EntityName, Action: role.Action, AccessType: role.AccessType, + Cluster: role.Cluster, + Namespace: role.Namespace, + Group: role.Group, + Kind: role.Kind, + Resource: role.Resource, }) } } @@ -501,7 +615,7 @@ func (impl UserServiceImpl) mergeGroups(oldGroups []string, newGroups []string) return groups } -func (impl UserServiceImpl) UpdateUser(userInfo *bean.UserInfo, token string, managerAuth func(token string, object string) bool) (*bean.UserInfo, bool, bool, []string, error) { +func (impl UserServiceImpl) UpdateUser(userInfo *bean.UserInfo, token string, managerAuth func(resource, token string, object string) bool) (*bean.UserInfo, bool, bool, []string, error) { //validating if action user is not admin and trying to update user who has super admin polices, return 403 isUserSuperAdmin, err := impl.IsSuperAdmin(int(userInfo.Id)) if err != nil { @@ -572,97 +686,107 @@ func (impl UserServiceImpl) UpdateUser(userInfo *bean.UserInfo, token string, ma //Adding New Policies for _, roleFilter := range userInfo.RoleFilters { - if len(roleFilter.Team) > 0 { - // check auth only for apps permission, skip for chart group - rbacObject := fmt.Sprintf("%s", strings.ToLower(roleFilter.Team)) - isValidAuth := managerAuth(token, rbacObject) - if !isValidAuth { - continue + if roleFilter.Entity == bean.CLUSTER_ENTITIY { + policiesToBeAdded, rolesChangedFromRoleUpdate, err := impl.CreateOrUpdateUserRolesForClusterEntity(roleFilter, userInfo.UserId, model, existingRoleIds, token, managerAuth, tx) + if err != nil { + impl.logger.Errorw("error in creating user roles for clusterEntity", "err", err) + return nil, false, false, nil, err } - } - - if roleFilter.EntityName == "" { - roleFilter.EntityName = "NONE" - } - if roleFilter.Environment == "" { - roleFilter.Environment = "NONE" - } - entityNames := strings.Split(roleFilter.EntityName, ",") - environments := strings.Split(roleFilter.Environment, ",") - for _, environment := range environments { - for _, entityName := range entityNames { - if entityName == "NONE" { - entityName = "" - } - if environment == "NONE" { - environment = "" - } - roleModel, err := impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) - if err != nil { - impl.logger.Errorw("Error in fetching role by filter", "user", userInfo) - return nil, false, false, nil, err + addedPolicies = append(addedPolicies, policiesToBeAdded...) + rolesChanged = rolesChangedFromRoleUpdate + } else { + if len(roleFilter.Team) > 0 { + // check auth only for apps permission, skip for chart group + rbacObject := fmt.Sprintf("%s", strings.ToLower(roleFilter.Team)) + isValidAuth := managerAuth(casbin2.ResourceUser, token, rbacObject) + if !isValidAuth { + continue } - if roleModel.Id == 0 { - impl.logger.Debugw("no role found for given filter", "filter", roleFilter) - userInfo.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + } - if len(roleFilter.Team) > 0 { - if roleFilter.AccessType == bean.APP_ACCESS_TYPE_HELM { - flag, err := impl.userAuthRepository.CreateDefaultHelmPolicies(roleFilter.Team, entityName, environment, tx) - if err != nil || flag == false { + if roleFilter.EntityName == "" { + roleFilter.EntityName = "NONE" + } + if roleFilter.Environment == "" { + roleFilter.Environment = "NONE" + } + entityNames := strings.Split(roleFilter.EntityName, ",") + environments := strings.Split(roleFilter.Environment, ",") + for _, environment := range environments { + for _, entityName := range entityNames { + if entityName == "NONE" { + entityName = "" + } + if environment == "NONE" { + environment = "" + } + roleModel, err := impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "user", userInfo) + return nil, false, false, nil, err + } + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + userInfo.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + + if len(roleFilter.Team) > 0 { + if roleFilter.AccessType == bean.APP_ACCESS_TYPE_HELM { + flag, err := impl.userAuthRepository.CreateDefaultHelmPolicies(roleFilter.Team, entityName, environment, tx) + if err != nil || flag == false { + return nil, false, false, nil, err + } + } else { + flag, err := impl.userAuthRepository.CreateDefaultPolicies(roleFilter.Team, entityName, environment, tx) + if err != nil || flag == false { + return nil, false, false, nil, err + } + } + roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "user", userInfo) return nil, false, false, nil, err } - } else { - flag, err := impl.userAuthRepository.CreateDefaultPolicies(roleFilter.Team, entityName, environment, tx) + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + userInfo.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + continue + } + } else if len(roleFilter.Entity) > 0 { + flag, err := impl.userAuthRepository.CreateDefaultPoliciesForGlobalEntity(roleFilter.Entity, entityName, roleFilter.Action, tx) if err != nil || flag == false { return nil, false, false, nil, err } - } - roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) - if err != nil { - impl.logger.Errorw("Error in fetching role by filter", "user", userInfo) - return nil, false, false, nil, err - } - if roleModel.Id == 0 { - impl.logger.Debugw("no role found for given filter", "filter", roleFilter) - userInfo.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action - continue - } - } else if len(roleFilter.Entity) > 0 { - flag, err := impl.userAuthRepository.CreateDefaultPoliciesForGlobalEntity(roleFilter.Entity, entityName, roleFilter.Action, tx) - if err != nil || flag == false { - return nil, false, false, nil, err - } - roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) - if err != nil { - impl.logger.Errorw("Error in fetching role by filter", "user", userInfo) - return nil, false, false, nil, err - } - if roleModel.Id == 0 { - impl.logger.Debugw("no role found for given filter", "filter", roleFilter) - userInfo.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + roleModel, err = impl.userAuthRepository.GetRoleByFilter(roleFilter.Entity, roleFilter.Team, entityName, environment, roleFilter.Action, roleFilter.AccessType) + if err != nil { + impl.logger.Errorw("Error in fetching role by filter", "user", userInfo) + return nil, false, false, nil, err + } + if roleModel.Id == 0 { + impl.logger.Debugw("no role found for given filter", "filter", roleFilter) + userInfo.Status = "role not fount for any given filter: " + roleFilter.Team + "," + environment + "," + entityName + "," + roleFilter.Action + continue + } + } else { continue } - } else { - continue } - } - if _, ok := existingRoleIds[roleModel.Id]; ok { - //Adding policies which is removed - addedPolicies = append(addedPolicies, casbin2.Policy{Type: "g", Sub: casbin2.Subject(model.EmailId), Obj: casbin2.Object(roleModel.Role)}) - } else { - if roleModel.Id > 0 { - rolesChanged = true - userRoleModel := &repository2.UserRoleModel{UserId: model.Id, RoleId: roleModel.Id} - userRoleModel.CreatedBy = userInfo.UserId - userRoleModel.UpdatedBy = userInfo.UserId - userRoleModel.CreatedOn = time.Now() - userRoleModel.UpdatedOn = time.Now() - userRoleModel, err = impl.userAuthRepository.CreateUserRoleMapping(userRoleModel, tx) - if err != nil { - return nil, false, false, nil, err - } + if _, ok := existingRoleIds[roleModel.Id]; ok { + //Adding policies which is removed addedPolicies = append(addedPolicies, casbin2.Policy{Type: "g", Sub: casbin2.Subject(model.EmailId), Obj: casbin2.Object(roleModel.Role)}) + } else { + if roleModel.Id > 0 { + rolesChanged = true + userRoleModel := &repository2.UserRoleModel{UserId: model.Id, RoleId: roleModel.Id} + userRoleModel.CreatedBy = userInfo.UserId + userRoleModel.UpdatedBy = userInfo.UserId + userRoleModel.CreatedOn = time.Now() + userRoleModel.UpdatedOn = time.Now() + userRoleModel, err = impl.userAuthRepository.CreateUserRoleMapping(userRoleModel, tx) + if err != nil { + return nil, false, false, nil, err + } + addedPolicies = append(addedPolicies, casbin2.Policy{Type: "g", Sub: casbin2.Subject(model.EmailId), Obj: casbin2.Object(roleModel.Role)}) + } } } } @@ -799,18 +923,50 @@ func (impl UserServiceImpl) getUserMetadata(model *repository2.UserModel) (bool, if len(role.Team) > 0 { key = fmt.Sprintf("%s_%s_%s", role.Team, role.Action, role.AccessType) } else if len(role.Entity) > 0 { - key = fmt.Sprintf("%s_%s_%s", role.Entity, role.Action) + if role.Entity == bean.CLUSTER_ENTITIY { + key = fmt.Sprintf("%s_%s_%s_%s_%s_%s", role.Entity, role.Action, role.Cluster, + role.Namespace, role.Group, role.Kind) + } else { + key = fmt.Sprintf("%s_%s_%s", role.Entity, role.Action) + } } if _, ok := roleFilterMap[key]; ok { - envArr := strings.Split(roleFilterMap[key].Environment, ",") - if containsArr(envArr, AllEnvironment) { - roleFilterMap[key].Environment = AllEnvironment - } else if !containsArr(envArr, role.Environment) { - roleFilterMap[key].Environment = fmt.Sprintf("%s,%s", roleFilterMap[key].Environment, role.Environment) - } - entityArr := strings.Split(roleFilterMap[key].EntityName, ",") - if !containsArr(entityArr, role.EntityName) { - roleFilterMap[key].EntityName = fmt.Sprintf("%s,%s", roleFilterMap[key].EntityName, role.EntityName) + if role.Entity == bean.CLUSTER_ENTITIY { + namespaceArr := strings.Split(roleFilterMap[key].Namespace, ",") + if containsArr(namespaceArr, AllNamespace) { + roleFilterMap[key].Namespace = AllNamespace + } else if !containsArr(namespaceArr, role.Namespace) { + roleFilterMap[key].Namespace = fmt.Sprintf("%s,%s", roleFilterMap[key].Namespace, role.Namespace) + } + groupArr := strings.Split(roleFilterMap[key].Group, ",") + if containsArr(groupArr, AllGroup) { + roleFilterMap[key].Group = AllGroup + } else if !containsArr(groupArr, role.Group) { + roleFilterMap[key].Group = fmt.Sprintf("%s,%s", roleFilterMap[key].Group, role.Group) + } + kindArr := strings.Split(roleFilterMap[key].Kind, ",") + if containsArr(kindArr, AllKind) { + roleFilterMap[key].Kind = AllKind + } else if !containsArr(kindArr, role.Kind) { + roleFilterMap[key].Kind = fmt.Sprintf("%s,%s", roleFilterMap[key].Kind, role.Kind) + } + resourceArr := strings.Split(roleFilterMap[key].Resource, ",") + if containsArr(resourceArr, AllResource) { + roleFilterMap[key].Resource = AllResource + } else if !containsArr(resourceArr, role.Resource) { + roleFilterMap[key].Resource = fmt.Sprintf("%s,%s", roleFilterMap[key].Resource, role.Resource) + } + } else { + envArr := strings.Split(roleFilterMap[key].Environment, ",") + if containsArr(envArr, AllEnvironment) { + roleFilterMap[key].Environment = AllEnvironment + } else if !containsArr(envArr, role.Environment) { + roleFilterMap[key].Environment = fmt.Sprintf("%s,%s", roleFilterMap[key].Environment, role.Environment) + } + entityArr := strings.Split(roleFilterMap[key].EntityName, ",") + if !containsArr(entityArr, role.EntityName) { + roleFilterMap[key].EntityName = fmt.Sprintf("%s,%s", roleFilterMap[key].EntityName, role.EntityName) + } } } else { roleFilterMap[key] = &bean.RoleFilter{ @@ -820,6 +976,11 @@ func (impl UserServiceImpl) getUserMetadata(model *repository2.UserModel) (bool, EntityName: role.EntityName, Action: role.Action, AccessType: role.AccessType, + Cluster: role.Cluster, + Namespace: role.Namespace, + Group: role.Group, + Kind: role.Kind, + Resource: role.Resource, } } @@ -969,6 +1130,11 @@ func (impl UserServiceImpl) GetUserByEmail(emailId string) (*bean.UserInfo, erro Environment: role.Environment, EntityName: role.EntityName, Action: role.Action, + Cluster: role.Cluster, + Namespace: role.Namespace, + Group: role.Group, + Kind: role.Kind, + Resource: role.Resource, }) } @@ -1220,7 +1386,7 @@ func (impl UserServiceImpl) saveUserAudit(r *http.Request, userId int32) { impl.userAuditService.Save(userAudit) } -func (impl UserServiceImpl) checkGroupAuth(groupName string, token string, managerAuth func(token string, object string) bool, isActionUserSuperAdmin bool) bool { +func (impl UserServiceImpl) checkGroupAuth(groupName string, token string, managerAuth func(resource, token string, object string) bool, isActionUserSuperAdmin bool) bool { //check permission for group which is going to add/eliminate roles, err := impl.roleGroupRepository.GetRolesByGroupCasbinName(groupName) if err != nil && err != pg.ErrNoRows { @@ -1234,12 +1400,18 @@ func (impl UserServiceImpl) checkGroupAuth(groupName string, token string, manag } if len(role.Team) > 0 { rbacObject := fmt.Sprintf("%s", strings.ToLower(role.Team)) - isValidAuth := managerAuth(token, rbacObject) + isValidAuth := managerAuth(casbin2.ResourceUser, token, rbacObject) if !isValidAuth { hasAccessToGroup = false - continue } } + if role.Entity == bean.CLUSTER_ENTITIY && !isActionUserSuperAdmin { + isValidAuth := impl.userCommonService.CheckRbacForClusterEntity(role.Cluster, role.Namespace, role.Group, role.Kind, role.Resource, token, managerAuth) + if !isValidAuth { + hasAccessToGroup = false + } + } + } return hasAccessToGroup } @@ -1257,18 +1429,50 @@ func (impl UserServiceImpl) GetRoleFiltersByGroupNames(groupNames []string) ([]b if len(role.Team) > 0 { key = fmt.Sprintf("%s_%s_%s", role.Team, role.Action, role.AccessType) } else if len(role.Entity) > 0 { - key = fmt.Sprintf("%s_%s_%s", role.Entity, role.Action) + if role.Entity == bean.CLUSTER_ENTITIY { + key = fmt.Sprintf("%s_%s_%s_%s_%s_%s", role.Entity, role.Action, role.Cluster, + role.Namespace, role.Group, role.Kind) + } else { + key = fmt.Sprintf("%s_%s_%s", role.Entity, role.Action) + } } if _, ok := roleFilterMap[key]; ok { - envArr := strings.Split(roleFilterMap[key].Environment, ",") - if containsArr(envArr, AllEnvironment) { - roleFilterMap[key].Environment = AllEnvironment - } else if !containsArr(envArr, role.Environment) { - roleFilterMap[key].Environment = fmt.Sprintf("%s,%s", roleFilterMap[key].Environment, role.Environment) - } - entityArr := strings.Split(roleFilterMap[key].EntityName, ",") - if !containsArr(entityArr, role.EntityName) { - roleFilterMap[key].EntityName = fmt.Sprintf("%s,%s", roleFilterMap[key].EntityName, role.EntityName) + if role.Entity == bean.CLUSTER_ENTITIY { + namespaceArr := strings.Split(roleFilterMap[key].Namespace, ",") + if containsArr(namespaceArr, AllNamespace) { + roleFilterMap[key].Namespace = AllNamespace + } else if !containsArr(namespaceArr, role.Namespace) { + roleFilterMap[key].Namespace = fmt.Sprintf("%s,%s", roleFilterMap[key].Namespace, role.Namespace) + } + groupArr := strings.Split(roleFilterMap[key].Group, ",") + if containsArr(groupArr, AllGroup) { + roleFilterMap[key].Group = AllGroup + } else if !containsArr(groupArr, role.Group) { + roleFilterMap[key].Group = fmt.Sprintf("%s,%s", roleFilterMap[key].Group, role.Group) + } + kindArr := strings.Split(roleFilterMap[key].Kind, ",") + if containsArr(kindArr, AllKind) { + roleFilterMap[key].Kind = AllKind + } else if !containsArr(kindArr, role.Kind) { + roleFilterMap[key].Kind = fmt.Sprintf("%s,%s", roleFilterMap[key].Kind, role.Kind) + } + resourceArr := strings.Split(roleFilterMap[key].Resource, ",") + if containsArr(resourceArr, AllResource) { + roleFilterMap[key].Resource = AllResource + } else if !containsArr(resourceArr, role.Resource) { + roleFilterMap[key].Resource = fmt.Sprintf("%s,%s", roleFilterMap[key].Resource, role.Resource) + } + } else { + envArr := strings.Split(roleFilterMap[key].Environment, ",") + if containsArr(envArr, AllEnvironment) { + roleFilterMap[key].Environment = AllEnvironment + } else if !containsArr(envArr, role.Environment) { + roleFilterMap[key].Environment = fmt.Sprintf("%s,%s", roleFilterMap[key].Environment, role.Environment) + } + entityArr := strings.Split(roleFilterMap[key].EntityName, ",") + if !containsArr(entityArr, role.EntityName) { + roleFilterMap[key].EntityName = fmt.Sprintf("%s,%s", roleFilterMap[key].EntityName, role.EntityName) + } } } else { roleFilterMap[key] = &bean.RoleFilter{ @@ -1278,6 +1482,11 @@ func (impl UserServiceImpl) GetRoleFiltersByGroupNames(groupNames []string) ([]b EntityName: role.EntityName, Action: role.Action, AccessType: role.AccessType, + Cluster: role.Cluster, + Namespace: role.Namespace, + Group: role.Group, + Kind: role.Kind, + Resource: role.Resource, } } diff --git a/pkg/user/casbin/rbacpolicy.go b/pkg/user/casbin/rbacpolicy.go index 58a28518cf..5755a3b2e3 100644 --- a/pkg/user/casbin/rbacpolicy.go +++ b/pkg/user/casbin/rbacpolicy.go @@ -49,4 +49,8 @@ const ( ActionTrigger = "trigger" ActionNotify = "notify" ActionExec = "exec" + + ClusterResourceRegex = "%s/%s" // {cluster}/{namespace} + ClusterObjectRegex = "%s/%s/%s" // {groupName}/{kindName}/{objectName} + ClusterEmptyGroupPlaceholder = "k8sempty" ) diff --git a/pkg/user/repository/DefaultAuthPolicyRepository.go b/pkg/user/repository/DefaultAuthPolicyRepository.go index c596fe83ea..394eee254b 100644 --- a/pkg/user/repository/DefaultAuthPolicyRepository.go +++ b/pkg/user/repository/DefaultAuthPolicyRepository.go @@ -19,6 +19,9 @@ const ( ENTITY_SPECIFIC_ADMIN_TYPE RoleType = "entitySpecificAdmin" ENTITY_SPECIFIC_VIEW_TYPE RoleType = "entitySpecificView" ROLE_SPECIFIC_TYPE RoleType = "roleSpecific" + ENTITY_CLUSTER_ADMIN_TYPE RoleType = "clusterAdmin" + ENTITY_CLUSTER_EDIT_TYPE RoleType = "clusterEdit" + ENTITY_CLUSTER_VIEW_TYPE RoleType = "clusterView" ) type DefaultAuthPolicyRepository interface { diff --git a/pkg/user/repository/RoleGroupRepository.go b/pkg/user/repository/RoleGroupRepository.go index 2177b35249..24cc0ccc08 100644 --- a/pkg/user/repository/RoleGroupRepository.go +++ b/pkg/user/repository/RoleGroupRepository.go @@ -42,6 +42,7 @@ type RoleGroupRepository interface { GetRoleGroupRoleMappingByRoleGroupIds(roleGroupIds []int32) ([]*RoleModel, error) GetRolesByGroupCasbinName(groupName string) ([]*RoleModel, error) GetRolesByGroupNames(groupNames []string) ([]*RoleModel, error) + GetRolesByGroupNamesAndEntity(groupNames []string, entity string) ([]*RoleModel, error) } type RoleGroupRepositoryImpl struct { @@ -216,3 +217,17 @@ func (impl RoleGroupRepositoryImpl) GetRolesByGroupNames(groupNames []string) ([ } return roleModels, nil } + +func (impl RoleGroupRepositoryImpl) GetRolesByGroupNamesAndEntity(groupNames []string, entity string) ([]*RoleModel, error) { + var roleModels []*RoleModel + query := "SELECT r.* from roles r" + + " INNER JOIN role_group_role_mapping rgm on rgm.role_id=r.id" + + " INNER JOIN role_group rg on rg.id=rgm.role_group_id" + + " WHERE rg.casbin_name in (?) and r.entity=?;" + _, err := impl.dbConnection.Query(&roleModels, query, pg.In(groupNames), entity) + if err != nil { + impl.Logger.Errorw("error in getting roles by group names", "err", err, "groupNames", groupNames) + return roleModels, err + } + return roleModels, nil +} diff --git a/pkg/user/repository/UserAuthRepository.go b/pkg/user/repository/UserAuthRepository.go index 432e9b97af..2fb3d6d828 100644 --- a/pkg/user/repository/UserAuthRepository.go +++ b/pkg/user/repository/UserAuthRepository.go @@ -16,7 +16,7 @@ */ /* - @description: user authentication and authorization +@description: user authentication and authorization */ package repository @@ -56,6 +56,7 @@ type UserAuthRepository interface { CreateDefaultPolicies(team string, entityName string, env string, tx *pg.Tx) (bool, error) CreateDefaultHelmPolicies(team string, entityName string, env string, tx *pg.Tx) (bool, error) CreateDefaultPoliciesForGlobalEntity(entity string, entityName string, action string, tx *pg.Tx) (bool, error) + CreateDefaultPoliciesForClusterEntity(entity, cluster, namespace, group, kind, resource string, tx *pg.Tx) (bool, error) CreateRoleForSuperAdminIfNotExists(tx *pg.Tx) (bool, error) SyncOrchestratorToCasbin(team string, entityName string, env string, tx *pg.Tx) (bool, error) UpdateTriggerPolicyForTerminalAccess() error @@ -64,6 +65,9 @@ type UserAuthRepository interface { GetRolesForApp(appName string) ([]*RoleModel, error) GetRolesForChartGroup(chartGroupName string) ([]*RoleModel, error) DeleteRole(role *RoleModel, tx *pg.Tx) error + + GetRoleByFilterForClusterEntity(cluster, namespace, group, kind, resource, action string) (RoleModel, error) + GetRolesByUserIdAndEntityType(userId int32, entityType string) ([]*RoleModel, error) } type UserAuthRepositoryImpl struct { @@ -94,6 +98,11 @@ type RoleModel struct { Environment string `sql:"environment"` Action string `sql:"action"` AccessType string `sql:"access_type"` + Cluster string `sql:"cluster"` + Namespace string `sql:"namespace"` + Group string `sql:"group"` + Kind string `sql:"kind"` + Resource string `sql:"resource"` sql.AuditLog } @@ -108,6 +117,20 @@ type RolePolicyDetails struct { EntityName string } +type ClusterRolePolicyDetails struct { + Entity string + Cluster string + Namespace string + Group string + Kind string + Resource string + ClusterObj string + NamespaceObj string + GroupObj string + KindObj string + ResourceObj string +} + func (impl UserAuthRepositoryImpl) CreateRole(userModel *RoleModel, tx *pg.Tx) (*RoleModel, error) { err := tx.Insert(userModel) if err != nil { @@ -281,6 +304,50 @@ func (impl UserAuthRepositoryImpl) GetRoleByFilter(entity string, team string, a return model, nil } +func (impl UserAuthRepositoryImpl) GetRoleByFilterForClusterEntity(cluster, namespace, group, kind, resource, action string) (RoleModel, error) { + var model RoleModel + query := "SELECT * FROM roles WHERE entity = ? " + var err error + + if len(cluster) > 0 { + query += " and cluster='" + cluster + "' " + } else { + query += " and cluster IS NULL " + } + if len(namespace) > 0 { + query += " and namespace='" + namespace + "' " + } else { + query += " and namespace IS NULL " + } + if len(group) > 0 { + query += " and \"group\"='" + group + "' " + } else { + query += " and \"group\" IS NULL " + } + if len(kind) > 0 { + query += " and kind='" + kind + "' " + } else { + query += " and kind IS NULL " + } + if len(resource) > 0 { + query += " and resource='" + resource + "' " + } else { + query += " and resource IS NULL " + } + if len(action) > 0 { + query += " and action='" + action + "' ;" + } else { + query += " and action IS NULL ;" + } + _, err = impl.dbConnection.Query(&model, query, bean.CLUSTER_ENTITIY) + if err != nil { + impl.Logger.Errorw("error in getting roles for clusterEntity", "err", err, + "cluster", cluster, "namespace", namespace, "kind", kind, "group", group, "resource", resource) + return model, err + } + return model, nil +} + func (impl UserAuthRepositoryImpl) CreateUserRoleMapping(userRoleModel *UserRoleModel, tx *pg.Tx) (*UserRoleModel, error) { err := tx.Insert(userRoleModel) if err != nil { @@ -845,6 +912,207 @@ func (impl UserAuthRepositoryImpl) CreateDefaultPoliciesForGlobalEntity(entity s return true, nil } +func (impl UserAuthRepositoryImpl) CreateDefaultPoliciesForClusterEntity(entity, cluster, namespace, group, kind, resource string, tx *pg.Tx) (bool, error) { + transaction, err := impl.dbConnection.Begin() + if err != nil { + return false, err + } + // Rollback tx on error. + defer transaction.Rollback() + + //getting policies from db + entityClusterAdminPolicyDb, err := impl.defaultAuthPolicyRepository.GetPolicyByRoleType(ENTITY_CLUSTER_ADMIN_TYPE) + if err != nil { + impl.Logger.Errorw("error in getting default policy by roleType", "err", err, "roleType", ENTITY_CLUSTER_ADMIN_TYPE) + return false, err + } + entityClusterEditPolicyDb, err := impl.defaultAuthPolicyRepository.GetPolicyByRoleType(ENTITY_CLUSTER_EDIT_TYPE) + if err != nil { + impl.Logger.Errorw("error in getting default policy by roleType", "err", err, "roleType", ENTITY_CLUSTER_EDIT_TYPE) + return false, err + } + entityClusterViewPolicyDb, err := impl.defaultAuthPolicyRepository.GetPolicyByRoleType(ENTITY_CLUSTER_VIEW_TYPE) + if err != nil { + impl.Logger.Errorw("error in getting default policy by roleType", "err", err, "roleType", ENTITY_CLUSTER_VIEW_TYPE) + return false, err + } + clusterObj := cluster + namespaceObj := namespace + groupObj := group + kindObj := kind + resourceObj := resource + + if cluster == "" { + clusterObj = "*" + } + if namespace == "" { + namespaceObj = "*" + } + if group == "" { + groupObj = "*" + } + if kind == "" { + kindObj = "*" + } + if resource == "" { + resourceObj = "*" + } + policyDetails := ClusterRolePolicyDetails{ + Entity: entity, + Cluster: cluster, + Namespace: namespace, + Group: group, + Kind: kind, + Resource: resource, + ClusterObj: clusterObj, + NamespaceObj: namespaceObj, + GroupObj: groupObj, + KindObj: kindObj, + ResourceObj: resourceObj, + } + + //getting updated clusterAdmin policies + entityClusterAdminPolicy, err := util.Tprintf(entityClusterAdminPolicyDb, policyDetails) + if err != nil { + impl.Logger.Errorw("error in getting updated policies", "err", err, "roleType", ENTITY_CLUSTER_ADMIN_TYPE) + return false, err + } + + //getting updated clusterEdit policies + entityClusterEditPolicy, err := util.Tprintf(entityClusterEditPolicyDb, policyDetails) + if err != nil { + impl.Logger.Errorw("error in getting updated policies", "err", err, "roleType", ENTITY_CLUSTER_EDIT_TYPE) + return false, err + } + + //getting updated clusterView policies + entityClusterViewPolicy, err := util.Tprintf(entityClusterViewPolicyDb, policyDetails) + if err != nil { + impl.Logger.Errorw("error in getting updated policies", "err", err, "roleType", ENTITY_CLUSTER_VIEW_TYPE) + return false, err + } + + //for START in Casbin Object Ends Here + var policiesAdmin bean.PolicyRequest + err = json.Unmarshal([]byte(entityClusterAdminPolicy), &policiesAdmin) + if err != nil { + impl.Logger.Errorw("decode err", "err", err) + return false, err + } + impl.Logger.Debugw("add policy request", "policies", policiesAdmin) + casbin.AddPolicy(policiesAdmin.Data) + + var policiesEdit bean.PolicyRequest + err = json.Unmarshal([]byte(entityClusterEditPolicy), &policiesEdit) + if err != nil { + impl.Logger.Errorw("decode err", "err", err) + return false, err + } + impl.Logger.Debugw("add policy request", "policies", policiesEdit) + casbin.AddPolicy(policiesEdit.Data) + + var policiesView bean.PolicyRequest + err = json.Unmarshal([]byte(entityClusterViewPolicy), &policiesView) + if err != nil { + impl.Logger.Errorw("decode err", "err", err) + return false, err + } + impl.Logger.Debugw("add policy request", "policies", policiesView) + casbin.AddPolicy(policiesView.Data) + //CASBIN ENDS + + //Creating ROLES + + //getting role from db + clusterAdminRoleDb, err := impl.defaultAuthRoleRepository.GetRoleByRoleType(ENTITY_CLUSTER_ADMIN_TYPE) + if err != nil { + impl.Logger.Errorw("error in getting default policy by roleType", "err", err, "roleType", ENTITY_CLUSTER_ADMIN_TYPE) + return false, err + } + + //getting updated role + roleClusterAdmin, err := util.Tprintf(clusterAdminRoleDb, policyDetails) + if err != nil { + impl.Logger.Errorw("error in getting updated policies", "err", err, "roleType", ENTITY_CLUSTER_ADMIN_TYPE) + return false, err + } + + //getting role from db + clusterEditRoleDb, err := impl.defaultAuthRoleRepository.GetRoleByRoleType(ENTITY_CLUSTER_EDIT_TYPE) + if err != nil { + impl.Logger.Errorw("error in getting default policy by roleType", "err", err, "roleType", ENTITY_CLUSTER_EDIT_TYPE) + return false, err + } + + //getting updated role + roleClusterEdit, err := util.Tprintf(clusterEditRoleDb, policyDetails) + if err != nil { + impl.Logger.Errorw("error in getting updated policies", "err", err, "roleType", ENTITY_CLUSTER_EDIT_TYPE) + return false, err + } + + //getting role from db + clusterViewRoleDb, err := impl.defaultAuthRoleRepository.GetRoleByRoleType(ENTITY_CLUSTER_VIEW_TYPE) + if err != nil { + impl.Logger.Errorw("error in getting default policy by roleType", "err", err, "roleType", ENTITY_CLUSTER_VIEW_TYPE) + return false, err + } + + //getting updated role + roleClusterView, err := util.Tprintf(clusterViewRoleDb, policyDetails) + if err != nil { + impl.Logger.Errorw("error in getting updated policies", "err", err, "roleType", ENTITY_CLUSTER_VIEW_TYPE) + return false, err + } + + var roleClusterAdminData bean.RoleData + err = json.Unmarshal([]byte(roleClusterAdmin), &roleClusterAdminData) + if err != nil { + impl.Logger.Errorw("decode err", "err", err) + return false, err + } + _, err = impl.GetRole(roleClusterAdminData.Role) + if err != nil || err == pg.ErrNoRows { + _, err = impl.createRole(&roleClusterAdminData, transaction) + if err != nil && strings.Contains("duplicate key value violates unique constraint", err.Error()) { + return false, err + } + } + + var roleClusterEditData bean.RoleData + err = json.Unmarshal([]byte(roleClusterEdit), &roleClusterEditData) + if err != nil { + impl.Logger.Errorw("decode err", "err", err) + return false, err + } + _, err = impl.GetRole(roleClusterEditData.Role) + if err != nil || err == pg.ErrNoRows { + _, err = impl.createRole(&roleClusterEditData, transaction) + if err != nil && strings.Contains("duplicate key value violates unique constraint", err.Error()) { + return false, err + } + } + + var roleClusterViewData bean.RoleData + err = json.Unmarshal([]byte(roleClusterView), &roleClusterViewData) + if err != nil { + impl.Logger.Errorw("decode err", "err", err) + return false, err + } + _, err = impl.GetRole(roleClusterViewData.Role) + if err != nil || err == pg.ErrNoRows { + _, err = impl.createRole(&roleClusterViewData, transaction) + if err != nil && strings.Contains("duplicate key value violates unique constraint", err.Error()) { + return false, err + } + } + err = transaction.Commit() + if err != nil { + return false, err + } + return true, nil +} + func (impl UserAuthRepositoryImpl) CreateRoleForSuperAdminIfNotExists(tx *pg.Tx) (bool, error) { transaction, err := impl.dbConnection.Begin() if err != nil { @@ -887,6 +1155,11 @@ func (impl UserAuthRepositoryImpl) createRole(roleData *bean.RoleData, tx *pg.Tx Environment: roleData.Environment, Action: roleData.Action, AccessType: roleData.AccessType, + Cluster: roleData.Cluster, + Namespace: roleData.Namespace, + Group: roleData.Group, + Kind: roleData.Kind, + Resource: roleData.Resource, } roleModel, err := impl.CreateRole(roleModel, tx) if err != nil || roleModel == nil { @@ -1241,3 +1514,17 @@ func (impl UserAuthRepositoryImpl) DeleteRole(role *RoleModel, tx *pg.Tx) error } return nil } + +func (impl UserAuthRepositoryImpl) GetRolesByUserIdAndEntityType(userId int32, entityType string) ([]*RoleModel, error) { + var models []*RoleModel + err := impl.dbConnection.Model(&models). + Column("role_model.*"). + Join("INNER JOIN user_roles ur on ur.role_id=role_model.id"). + Where("role_model.entity = ?", entityType). + Where("ur.user_id = ?", userId).Select() + if err != nil { + impl.Logger.Error(err) + return models, err + } + return models, nil +} diff --git a/scripts/casbin/4_cluster_access_policy_insert.down.sql b/scripts/casbin/4_cluster_access_policy_insert.down.sql new file mode 100644 index 0000000000..fdde5ecc85 --- /dev/null +++ b/scripts/casbin/4_cluster_access_policy_insert.down.sql @@ -0,0 +1,5 @@ +DELETE FROM "public"."casbin_rule" + WHERE ("p_type" = 'p' AND "v0" = 'role:super-admin___' AND "v1" = '*/*' AND "v2" = '*' AND "v3" = '*/*/*' AND "v4" = 'allow' AND "v5" = ''); + +DELETE FROM "public"."casbin_rule" +WHERE ("p_type" = 'p' AND "v0" = 'role:super-admin___' AND "v1" = '*/*/user' AND "v2" = '*' AND "v3" = '*/*/*' AND "v4" = 'allow' AND "v5" = ''); \ No newline at end of file diff --git a/scripts/casbin/4_cluster_access_policy_insert.up.sql b/scripts/casbin/4_cluster_access_policy_insert.up.sql new file mode 100644 index 0000000000..81c8e99fb6 --- /dev/null +++ b/scripts/casbin/4_cluster_access_policy_insert.up.sql @@ -0,0 +1,3 @@ +INSERT INTO "public"."casbin_rule" ("p_type", "v0", "v1", "v2", "v3", "v4", "v5") + VALUES ('p', 'role:super-admin___', '*/*', '*', '*/*/*', 'allow', ''), --- v1=cluster/ns, v2=group/kind/resource + ('p', 'role:super-admin___', '*/*/user', '*', '*/*/*', 'allow', '') ; --- v1=cluster/ns/user, v2=group/kind/resource \ No newline at end of file diff --git a/scripts/sql/104_cluster_access.down.sql b/scripts/sql/104_cluster_access.down.sql new file mode 100644 index 0000000000..558690f385 --- /dev/null +++ b/scripts/sql/104_cluster_access.down.sql @@ -0,0 +1,12 @@ +DELETE FROM "default_auth_role" + WHERE role_type in ('clusterAdmin','clusterEdit','clusterView'); + +ALTER TABLE "roles" + DROP COLUMN "cluster", + DROP COLUMN "namespace", + DROP COLUMN "group", + DROP COLUMN "kind", + DROP COLUMN "resource"; + +DELETE FROM "default_auth_policy" + WHERE role_type in ('clusterAdmin','clusterEdit','clusterView'); \ No newline at end of file diff --git a/scripts/sql/104_cluster_access.up.sql b/scripts/sql/104_cluster_access.up.sql new file mode 100644 index 0000000000..a47ddae70a --- /dev/null +++ b/scripts/sql/104_cluster_access.up.sql @@ -0,0 +1,95 @@ +INSERT INTO "public"."default_auth_policy" ("id", "role_type", "policy", "created_on", "created_by", "updated_on", "updated_by") VALUES +('8', 'clusterAdmin', '{ + "data": [ + { + "type": "p", + "sub": "role:clusterAdmin_{{.Cluster}}_{{.Namespace}}_{{.Group}}_{{.Kind}}_{{.Resource}}", + "res": "{{.ClusterObj}}/{{.NamespaceObj}}", + "act": "*", + "obj": "{{.GroupObj}}/{{.KindObj}}/{{.ResourceObj}}" + }, + { + "type": "p", + "sub": "role:clusterAdmin_{{.Cluster}}_{{.Namespace}}_{{.Group}}_{{.Kind}}_{{.Resource}}", + "res": "{{.ClusterObj}}/{{.NamespaceObj}}/user", + "act": "*", + "obj": "{{.GroupObj}}/{{.KindObj}}/{{.ResourceObj}}" + } + ] +}', 'now()', '1', 'now()', '1'), +('9', 'clusterEdit', '{ + "data": [ + { + "type": "p", + "sub": "role:clusterEdit_{{.Cluster}}_{{.Namespace}}_{{.Group}}_{{.Kind}}_{{.Resource}}", + "res": "{{.ClusterObj}}/{{.NamespaceObj}}", + "act": "*", + "obj": "{{.GroupObj}}/{{.KindObj}}/{{.ResourceObj}}" + } + ] +}', 'now()', '1', 'now()', '1'), +('10', 'clusterView', '{ + "data": [ + { + "type": "p", + "sub": "role:clusterView_{{.Cluster}}_{{.Namespace}}_{{.Group}}_{{.Kind}}_{{.Resource}}", + "res": "{{.ClusterObj}}/{{.NamespaceObj}}", + "act": "get", + "obj": "{{.GroupObj}}/{{.KindObj}}/{{.ResourceObj}}" + } + ] +}', 'now()', '1', 'now()', '1'); + + +ALTER TABLE "roles" + ADD COLUMN "cluster" text, + ADD COLUMN "namespace" text, + ADD COLUMN "group" text, + ADD COLUMN "kind" text, + ADD COLUMN "resource" text; + + + +INSERT INTO "public"."default_auth_role" ("id", "role_type", "role", "created_on", "created_by", "updated_on", "updated_by") VALUES +('8', 'clusterAdmin', '{ + "role": "role:clusterAdmin_{{.Cluster}}_{{.Namespace}}_{{.Group}}_{{.Kind}}_{{.Resource}}", + "casbinSubjects": [ + "role:role:clusterAdmin_{{.Cluster}}_{{.Namespace}}_{{.Group}}_{{.Kind}}_{{.Resource}}" + ], + "entity": "{{.Entity}}", + "cluster": "{{.Cluster}}", + "namespace": "{{.Namespace}}", + "group": "{{.Group}}", + "kind": "{{.Kind}}", + "resource": "{{.Resource}}", + "action": "admin", + "access_type": "" +}', 'now()', '1', 'now()', '1'), +('9', 'clusterEdit', '{ + "role": "role:clusterEdit_{{.Cluster}}_{{.Namespace}}_{{.Group}}_{{.Kind}}_{{.Resource}}", + "casbinSubjects": [ + "role:clusterEdit_{{.Cluster}}_{{.Namespace}}_{{.Group}}_{{.Kind}}_{{.Resource}}" + ], + "entity": "{{.Entity}}", + "cluster": "{{.Cluster}}", + "namespace": "{{.Namespace}}", + "group": "{{.Group}}", + "kind": "{{.Kind}}", + "resource": "{{.Resource}}", + "action": "edit", + "access_type": "" +}', 'now()', '1', 'now()', '1'), +('10', 'clusterView', '{ + "role": "role:clusterView_{{.Cluster}}_{{.Namespace}}_{{.Group}}_{{.Kind}}_{{.Resource}}", + "casbinSubjects": [ + "role:clusterView_{{.Cluster}}_{{.Namespace}}_{{.Group}}_{{.Kind}}_{{.Resource}}" + ], + "entity": "{{.Entity}}", + "cluster": "{{.Cluster}}", + "namespace": "{{.Namespace}}", + "group": "{{.Group}}", + "kind": "{{.Kind}}", + "resource": "{{.Resource}}", + "action": "view", + "access_type": "" +}', 'now()', '1', 'now()', '1'); \ No newline at end of file diff --git a/specs/cluster_access_policy.yaml b/specs/cluster_access_policy.yaml new file mode 100644 index 0000000000..a8d71c2449 --- /dev/null +++ b/specs/cluster_access_policy.yaml @@ -0,0 +1,171 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Cluster access policy +paths: + /orchestrator/user: + post: + summary: Creates a new User + operationId: addUser + requestBody: + description: json as request body + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/User' + responses: + '200': + description: create user response + content: + application/json: + schema: + $ref: '#/components/schemas/User' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + put: + summary: update user + operationId: updateUser + requestBody: + description: json as request body + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/User' + responses: + '200': + description: user response + content: + application/json: + schema: + $ref: '#/components/schemas/User' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /orchestrator/role/group: + post: + summary: Creates a new role group + operationId: addUser + requestBody: + description: json as request body + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RoleGroup' + responses: + '200': + description: create user response + content: + application/json: + schema: + $ref: '#/components/schemas/RoleGroup' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + put: + summary: update user + operationId: updateUser + requestBody: + description: json as request body + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RoleGroup' + responses: + '200': + description: user response + content: + application/json: + schema: + $ref: '#/components/schemas/RoleGroup' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + +components: + schemas: + User: + type: object + required: + - email_id + properties: + id: + type: integer + description: Unique id of user + email_id: + 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/roleFilter' + description: role filters objects + RoleGroup: + type: object + properties: + id: + type: integer + name: + type: string + roleFilters: + type: array + items: + $ref: '#/components/schemas/roleFilter' + description: role filters objects + roleFilter: + type: object + required: + - action + properties: + cluster: + type: string + description: cluster name + namespace: + type: string + description: namespace names. for multiple selection comma separated values, for all selection an empty string. + group: + type: string + description: group names. for multiple selection comma separated values, for all selection an empty string. + kind: + type: string + description: kind names. for multiple selection comma separated values, for all selection an empty string. + resource: + type: string + description: resource names. for multiple selection comma separated values, for all selection an empty string. + action: + type: string + description: action is type of role, i.e, admin, trigger, view, etc. + enum: ["view","edit","admin"] + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + description: Error code + message: + type: string + description: Error message \ No newline at end of file diff --git a/specs/cluster_api_spec.yaml b/specs/cluster_api_spec.yaml new file mode 100644 index 0000000000..40650fa5cd --- /dev/null +++ b/specs/cluster_api_spec.yaml @@ -0,0 +1,82 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Devtron Labs +paths: + /orchestrator/cluster/auth-list: + get: + description: list of accessible cluster + responses: + '200': + description: cluster list + content: + application/json: + schema: + properties: + code: + type: integer + description: status code + status: + type: string + description: status + result: + type: array + description: namespace list group by cluster + items: + $ref: '#/components/schemas/Cluster' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + +# components mentioned below +components: + schemas: + Cluster: + type: object + required: + - key + - value + properties: + clusterId: + type: integer + description: cluster id + clusterName: + type: string + description: cluster name + + ErrorResponse: + required: + - code + - status + properties: + code: + type: integer + format: int32 + description: Error code + status: + type: string + description: Error message + errors: + type: array + description: errors + items: + $ref: '#/components/schemas/Error' + + Error: + required: + - code + - status + properties: + code: + type: integer + format: int32 + description: Error internal code + internalMessage: + type: string + description: Error internal message + userMessage: + type: string + description: Error user message \ No newline at end of file diff --git a/specs/external-app/appdetail-resource.yaml b/specs/k8s_apis-spec.yaml similarity index 68% rename from specs/external-app/appdetail-resource.yaml rename to specs/k8s_apis-spec.yaml index e000d7650c..84456b5a52 100644 --- a/specs/external-app/appdetail-resource.yaml +++ b/specs/k8s_apis-spec.yaml @@ -134,7 +134,18 @@ paths: type: string - name: appId in: query - required: true + required: false + schema: + type: string + - name: clusterId + in: query + required: false + schema: + type: integer + - name: namespace + in: query + description: it is required when clusterId is passed + required: false schema: type: string - name: follow @@ -156,17 +167,17 @@ paths: text/event-stream: schema: $ref: "#/components/schemas/LogsResponseObject" - /orchestrator/k8s/pod/exec/session/{applicationId}/{namespace}/{pod}/{shell}/{container}: + /orchestrator/k8s/pod/exec/session/{identifier}/{namespace}/{pod}/{shell}/{container}: get: description: get session for the terminal parameters: - in: path - name: applicationId + name: identifier schema: type: string required: true - description: application id - example: "2|devtroncd|devtron" + description: application id or cluster id + example: "2|devtroncd|devtron or 3" - in: path name: namespace schema: @@ -207,6 +218,71 @@ paths: application/json: schema: $ref: "#/components/schemas/TerminalMessage" + /orchestrator/k8s/api-resources/{clusterId}: + get: + description: Get All api resources for given cluster Id + parameters: + - name: clusterId + in: path + description: cluster Id + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Successfully fetched All api resources for given cluster Id + content: + application/json: + schema: + $ref: "#/components/schemas/GetAllApiResourcesResponse" + /orchestrator/k8s/resource/list: + post: + description: this api will be used for fetching all kind of manifest. + requestBody: + description: json as request body + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ResourceRequestObject' + responses: + '200': + description: list response + content: + application/json: + schema: + properties: + code: + type: integer + description: status code + status: + type: string + description: status + result: + type: array + description: app list + items: + $ref: '#/components/schemas/ClusterResourceListResponse' + /orchestrator/k8s/resources/apply: + post: + description: this api will be used to apply the resources in cluster + requestBody: + description: json as request body + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ApplyResourcesRequest' + responses: + '200': + description: response in array of each resource + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ApplyResourcesResponse" components: schemas: TerminalMessage: @@ -223,6 +299,9 @@ components: properties: appId: type: string + clusterId: + type: number + description: clusterId is used when request is for direct cluster (when appId is not supplied) k8sRequest: $ref: '#/components/schemas/K8sRequestObject' K8sRequestObject: @@ -409,4 +488,95 @@ components: data: type: string time: - type: string \ No newline at end of file + type: string + GetAllApiResourcesResponse: + type: object + properties: + apiResources: + type: array + items: + $ref: "#/components/schemas/K8sApiResource" + allowedAll: + type: boolean + description: whether all api-resources allowed for this user + example: true + nullable: false + K8sApiResource: + type: object + properties: + gvk: + $ref: '#/components/schemas/GroupVersionKind' + namespaced: + type: boolean + description: whether this api resource is in namespaces scope or global + example: true + nullable: false + GroupVersionKind: + type: object + properties: + group: + type: string + description: group of the api-resource + example: "apps" + nullable: false + version: + type: string + description: version of the api-resource + example: "v1" + nullable: false + kind: + type: string + description: kind of the api-resource + example: "pod" + nullable: false + ClusterResourceListResponse: + type: object + properties: + headers: + type: array + items: + type: string + data: + type: array + items: + type: object + properties: + header-name: + type: string + description: each object from data key contains the objects keys length is equal to headers length + ApplyResourcesRequest: + type: object + properties: + clusterId: + type: number + description: cluster Id + example: 1 + nullable: false + manifest: + type: string + description: manifest of the resources (yamls saparated by ---) + example: "" + nullable: false + ApplyResourcesResponse: + type: object + properties: + kind: + type: string + description: kind of the resource + example: "pod" + nullable: false + name: + type: string + description: name of the resource + example: "someName" + nullable: false + error: + type: string + description: error in the operation of this resource + example: "someError" + nullable: true + isUpdate: + type: boolean + description: whether this resource was updated + example: true + nullable: false \ No newline at end of file diff --git a/util/k8s/bean.go b/util/k8s/bean.go index 6e4f010a9b..56911753a3 100644 --- a/util/k8s/bean.go +++ b/util/k8s/bean.go @@ -112,3 +112,7 @@ type NodeDrainHelper struct { DisableEviction bool `json:"disableEviction"` k8sClientSet *kubernetes.Clientset } + +const DEFAULT_NAMESPACE = "default" +const EVENT_K8S_KIND = "Event" +const LIST_VERB = "list" diff --git a/util/k8s/k8sApplicationRestHandler.go b/util/k8s/k8sApplicationRestHandler.go index 15d940dcd3..553dd49fb3 100644 --- a/util/k8s/k8sApplicationRestHandler.go +++ b/util/k8s/k8sApplicationRestHandler.go @@ -3,12 +3,14 @@ package k8s import ( "context" "encoding/json" + "errors" "fmt" "github.com/devtron-labs/devtron/api/bean" "github.com/devtron-labs/devtron/api/connector" client "github.com/devtron-labs/devtron/api/helm-app" "github.com/devtron-labs/devtron/api/restHandler/common" "github.com/devtron-labs/devtron/client/k8s/application" + util2 "github.com/devtron-labs/devtron/internal/util" "github.com/devtron-labs/devtron/pkg/cluster" "github.com/devtron-labs/devtron/pkg/terminal" "github.com/devtron-labs/devtron/pkg/user" @@ -19,10 +21,12 @@ import ( "github.com/gorilla/mux" errors2 "github.com/juju/errors" "go.uber.org/zap" + errors3 "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "net/http" "strconv" + "strings" ) type K8sApplicationRestHandler interface { @@ -35,14 +39,19 @@ type K8sApplicationRestHandler interface { GetTerminalSession(w http.ResponseWriter, r *http.Request) GetResourceInfo(w http.ResponseWriter, r *http.Request) GetHostUrlsByBatch(w http.ResponseWriter, r *http.Request) + GetAllApiResources(w http.ResponseWriter, r *http.Request) + GetResourceList(w http.ResponseWriter, r *http.Request) + ApplyResources(w http.ResponseWriter, r *http.Request) } + type K8sApplicationRestHandlerImpl struct { logger *zap.SugaredLogger k8sApplicationService K8sApplicationService pump connector.Pump terminalSessionHandler terminal.TerminalSessionHandler enforcer casbin.Enforcer - enforcerUtil rbac.EnforcerUtilHelm + enforcerUtil rbac.EnforcerUtil + enforcerUtilHelm rbac.EnforcerUtilHelm clusterService cluster.ClusterService helmAppService client.HelmAppService userService user.UserService @@ -51,7 +60,7 @@ type K8sApplicationRestHandlerImpl struct { func NewK8sApplicationRestHandlerImpl(logger *zap.SugaredLogger, k8sApplicationService K8sApplicationService, pump connector.Pump, terminalSessionHandler terminal.TerminalSessionHandler, - enforcer casbin.Enforcer, enforcerUtil rbac.EnforcerUtilHelm, clusterService cluster.ClusterService, + enforcer casbin.Enforcer, enforcerUtilHelm rbac.EnforcerUtilHelm, enforcerUtil rbac.EnforcerUtil, clusterService cluster.ClusterService, helmAppService client.HelmAppService, userService user.UserService) *K8sApplicationRestHandlerImpl { return &K8sApplicationRestHandlerImpl{ logger: logger, @@ -59,6 +68,7 @@ func NewK8sApplicationRestHandlerImpl(logger *zap.SugaredLogger, pump: pump, terminalSessionHandler: terminalSessionHandler, enforcer: enforcer, + enforcerUtilHelm: enforcerUtilHelm, enforcerUtil: enforcerUtil, helmAppService: helmAppService, clusterService: clusterService, @@ -75,29 +85,38 @@ func (handler *K8sApplicationRestHandlerImpl) GetResource(w http.ResponseWriter, common.WriteJsonResp(w, err, nil, http.StatusBadRequest) return } - appIdentifier, err := handler.helmAppService.DecodeAppId(request.AppId) - if err != nil { - handler.logger.Errorw("error in decoding appId", "err", err, "appId", request.AppId) - common.WriteJsonResp(w, err, nil, http.StatusBadRequest) - return - } - //setting appIdentifier value in request - request.AppIdentifier = appIdentifier - valid, err := handler.k8sApplicationService.ValidateResourceRequest(request.AppIdentifier, request.K8sRequest) - if err != nil || !valid { - handler.logger.Errorw("error in validating resource request", "err", err) - common.WriteJsonResp(w, err, nil, http.StatusBadRequest) - return - } - - // RBAC enforcer applying - rbacObject := handler.enforcerUtil.GetHelmObjectByClusterId(request.AppIdentifier.ClusterId, request.AppIdentifier.Namespace, request.AppIdentifier.ReleaseName) + rbacObject := "" token := r.Header.Get("token") - if ok := handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionGet, rbacObject); !ok { - common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) + if request.AppId != "" { + appIdentifier, err := handler.helmAppService.DecodeAppId(request.AppId) + if err != nil { + handler.logger.Errorw("error in decoding appId", "err", err, "appId", request.AppId) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + //setting appIdentifier value in request + request.AppIdentifier = appIdentifier + request.ClusterId = request.AppIdentifier.ClusterId + valid, err := handler.k8sApplicationService.ValidateResourceRequest(request.AppIdentifier, request.K8sRequest) + if err != nil || !valid { + handler.logger.Errorw("error in validating resource request", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + + rbacObject = handler.enforcerUtilHelm.GetHelmObjectByClusterId(request.AppIdentifier.ClusterId, request.AppIdentifier.Namespace, request.AppIdentifier.ReleaseName) + if ok := handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionGet, rbacObject); !ok { + common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) + return + } + } else if request.ClusterId > 0 { + if ok := handler.validateRbac(w, token, request, casbin.ActionGet); !ok { + return + } + } else { + common.WriteJsonResp(w, errors.New("can not resource manifest as target cluster is not provided"), nil, http.StatusBadRequest) return } - //RBAC enforcer Ends resource, err := handler.k8sApplicationService.GetResource(&request) if err != nil { @@ -106,8 +125,13 @@ func (handler *K8sApplicationRestHandlerImpl) GetResource(w http.ResponseWriter, return } - // Obfuscate secret if user does not have edit access - canUpdate := handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionUpdate, rbacObject) + canUpdate := true + if request.AppId != "" { + // Obfuscate secret if user does not have edit access + canUpdate = handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionUpdate, rbacObject) + } else if request.ClusterId > 0 { + canUpdate = handler.validateRbac(nil, token, request, casbin.ActionUpdate) + } if !canUpdate && resource != nil { modifiedManifest, err := k8sObjectsUtil.HideValuesIfSecret(&resource.Manifest) if err != nil { @@ -121,6 +145,23 @@ func (handler *K8sApplicationRestHandlerImpl) GetResource(w http.ResponseWriter, common.WriteJsonResp(w, nil, resource, http.StatusOK) } +func (handler *K8sApplicationRestHandlerImpl) validateRbac(w http.ResponseWriter, token string, request ResourceRequestBean, casbinAction string) bool { + clusterBean, err := handler.clusterService.FindById(request.ClusterId) + if err != nil { + if w != nil { + common.WriteJsonResp(w, errors.New("clusterId is not valid"), nil, http.StatusBadRequest) + } + return false + } + if ok := handler.verifyRbacForCluster(token, clusterBean.ClusterName, request, casbinAction); !ok { + if w != nil { + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + } + return false + } + return true +} + func (handler *K8sApplicationRestHandlerImpl) GetHostUrlsByBatch(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) clusterIdString := vars["appId"] @@ -194,7 +235,7 @@ func (handler *K8sApplicationRestHandlerImpl) CreateResource(w http.ResponseWrit //setting appIdentifier value in request request.AppIdentifier = appIdentifier // RBAC enforcer applying - rbacObject := handler.enforcerUtil.GetHelmObjectByClusterId(request.AppIdentifier.ClusterId, request.AppIdentifier.Namespace, request.AppIdentifier.ReleaseName) + rbacObject := handler.enforcerUtilHelm.GetHelmObjectByClusterId(request.AppIdentifier.ClusterId, request.AppIdentifier.Namespace, request.AppIdentifier.ReleaseName) token := r.Header.Get("token") if ok := handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionUpdate, rbacObject); !ok { common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) @@ -212,6 +253,7 @@ func (handler *K8sApplicationRestHandlerImpl) CreateResource(w http.ResponseWrit func (handler *K8sApplicationRestHandlerImpl) UpdateResource(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) + token := r.Header.Get("token") var request ResourceRequestBean err := decoder.Decode(&request) if err != nil { @@ -219,28 +261,41 @@ func (handler *K8sApplicationRestHandlerImpl) UpdateResource(w http.ResponseWrit common.WriteJsonResp(w, err, nil, http.StatusBadRequest) return } - appIdentifier, err := handler.helmAppService.DecodeAppId(request.AppId) - if err != nil { - handler.logger.Errorw("error in decoding appId", "err", err, "appId", request.AppId) - common.WriteJsonResp(w, err, nil, http.StatusBadRequest) - return - } - //setting appIdentifier value in request - request.AppIdentifier = appIdentifier - valid, err := handler.k8sApplicationService.ValidateResourceRequest(request.AppIdentifier, request.K8sRequest) - if err != nil || !valid { - handler.logger.Errorw("error in validating resource request", "err", err) - common.WriteJsonResp(w, err, nil, http.StatusBadRequest) - return - } - // RBAC enforcer applying - rbacObject := handler.enforcerUtil.GetHelmObjectByClusterId(request.AppIdentifier.ClusterId, request.AppIdentifier.Namespace, request.AppIdentifier.ReleaseName) - token := r.Header.Get("token") - if ok := handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionUpdate, rbacObject); !ok { - common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) + + if len(request.AppId) > 0 { + // assume it as helm release case in which appId is supplied + appIdentifier, err := handler.helmAppService.DecodeAppId(request.AppId) + if err != nil { + handler.logger.Errorw("error in decoding appId", "err", err, "appId", request.AppId) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + //setting appIdentifier value in request + request.AppIdentifier = appIdentifier + request.ClusterId = appIdentifier.ClusterId + valid, err := handler.k8sApplicationService.ValidateResourceRequest(request.AppIdentifier, request.K8sRequest) + if err != nil || !valid { + handler.logger.Errorw("error in validating resource request", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + // RBAC enforcer applying + rbacObject := handler.enforcerUtilHelm.GetHelmObjectByClusterId(request.AppIdentifier.ClusterId, request.AppIdentifier.Namespace, request.AppIdentifier.ReleaseName) + if ok := handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionUpdate, rbacObject); !ok { + common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) + return + } + //RBAC enforcer Ends + } else if request.ClusterId > 0 { + // assume direct update in cluster + if ok := handler.validateRbac(w, token, request, casbin.ActionUpdate); !ok { + return + } + } else { + common.WriteJsonResp(w, errors.New("can not update resource as target cluster is not provided"), nil, http.StatusBadRequest) return } - //RBAC enforcer Ends + resource, err := handler.k8sApplicationService.UpdateResource(&request) if err != nil { handler.logger.Errorw("error in updating resource", "err", err) @@ -252,6 +307,7 @@ func (handler *K8sApplicationRestHandlerImpl) UpdateResource(w http.ResponseWrit func (handler *K8sApplicationRestHandlerImpl) DeleteResource(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) + token := r.Header.Get("token") var request ResourceRequestBean err := decoder.Decode(&request) if err != nil { @@ -259,28 +315,40 @@ func (handler *K8sApplicationRestHandlerImpl) DeleteResource(w http.ResponseWrit common.WriteJsonResp(w, err, nil, http.StatusBadRequest) return } - appIdentifier, err := handler.helmAppService.DecodeAppId(request.AppId) - if err != nil { - handler.logger.Errorw("error in decoding appId", "err", err, "appId", request.AppId) - common.WriteJsonResp(w, err, nil, http.StatusBadRequest) - return - } - //setting appIdentifier value in request - request.AppIdentifier = appIdentifier - valid, err := handler.k8sApplicationService.ValidateResourceRequest(request.AppIdentifier, request.K8sRequest) - if err != nil || !valid { - handler.logger.Errorw("error in validating resource request", "err", err) - common.WriteJsonResp(w, err, nil, http.StatusBadRequest) - return - } - // RBAC enforcer applying - rbacObject := handler.enforcerUtil.GetHelmObjectByClusterId(request.AppIdentifier.ClusterId, request.AppIdentifier.Namespace, request.AppIdentifier.ReleaseName) - token := r.Header.Get("token") - if ok := handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionDelete, rbacObject); !ok { - common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) + + if len(request.AppId) > 0 { + // assume it as helm release case in which appId is supplied + appIdentifier, err := handler.helmAppService.DecodeAppId(request.AppId) + if err != nil { + handler.logger.Errorw("error in decoding appId", "err", err, "appId", request.AppId) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + //setting appIdentifier value in request + request.AppIdentifier = appIdentifier + request.ClusterId = appIdentifier.ClusterId + valid, err := handler.k8sApplicationService.ValidateResourceRequest(request.AppIdentifier, request.K8sRequest) + if err != nil || !valid { + handler.logger.Errorw("error in validating resource request", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + // RBAC enforcer applying + rbacObject := handler.enforcerUtilHelm.GetHelmObjectByClusterId(request.AppIdentifier.ClusterId, request.AppIdentifier.Namespace, request.AppIdentifier.ReleaseName) + if ok := handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionDelete, rbacObject); !ok { + common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) + return + } + //RBAC enforcer Ends + } else if request.ClusterId > 0 { + if ok := handler.validateRbac(w, token, request, casbin.ActionDelete); !ok { + return + } + } else { + common.WriteJsonResp(w, errors.New("can not delete resource as target cluster is not provided"), nil, http.StatusBadRequest) return } - //RBAC enforcer Ends + resource, err := handler.k8sApplicationService.DeleteResource(&request) if err != nil { handler.logger.Errorw("error in deleting resource", "err", err) @@ -292,6 +360,7 @@ func (handler *K8sApplicationRestHandlerImpl) DeleteResource(w http.ResponseWrit func (handler *K8sApplicationRestHandlerImpl) ListEvents(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) + token := r.Header.Get("token") var request ResourceRequestBean err := decoder.Decode(&request) if err != nil { @@ -299,28 +368,38 @@ func (handler *K8sApplicationRestHandlerImpl) ListEvents(w http.ResponseWriter, common.WriteJsonResp(w, err, nil, http.StatusBadRequest) return } - appIdentifier, err := handler.helmAppService.DecodeAppId(request.AppId) - if err != nil { - handler.logger.Errorw("error in decoding appId", "err", err, "appId", request.AppId) - common.WriteJsonResp(w, err, nil, http.StatusBadRequest) - return - } - //setting appIdentifier value in request - request.AppIdentifier = appIdentifier - valid, err := handler.k8sApplicationService.ValidateResourceRequest(request.AppIdentifier, request.K8sRequest) - if err != nil || !valid { - handler.logger.Errorw("error in validating resource request", "err", err) - common.WriteJsonResp(w, err, nil, http.StatusBadRequest) - return - } - // RBAC enforcer applying - rbacObject := handler.enforcerUtil.GetHelmObjectByClusterId(request.AppIdentifier.ClusterId, request.AppIdentifier.Namespace, request.AppIdentifier.ReleaseName) - token := r.Header.Get("token") - if ok := handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionGet, rbacObject); !ok { - common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) + if len(request.AppId) > 0 { + // assume it as helm release case in which appId is supplied + appIdentifier, err := handler.helmAppService.DecodeAppId(request.AppId) + if err != nil { + handler.logger.Errorw("error in decoding appId", "err", err, "appId", request.AppId) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + //setting appIdentifier value in request + request.AppIdentifier = appIdentifier + request.ClusterId = appIdentifier.ClusterId + valid, err := handler.k8sApplicationService.ValidateResourceRequest(request.AppIdentifier, request.K8sRequest) + if err != nil || !valid { + handler.logger.Errorw("error in validating resource request", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + // RBAC enforcer applying + rbacObject := handler.enforcerUtilHelm.GetHelmObjectByClusterId(request.AppIdentifier.ClusterId, request.AppIdentifier.Namespace, request.AppIdentifier.ReleaseName) + if ok := handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionGet, rbacObject); !ok { + common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) + return + } + //RBAC enforcer Ends + } else if request.ClusterId > 0 { + if ok := handler.validateRbac(w, token, request, casbin.ActionGet); !ok { + return + } + } else { + common.WriteJsonResp(w, errors.New("can not get resource as target cluster is not provided"), nil, http.StatusBadRequest) return } - //RBAC enforcer Ends events, err := handler.k8sApplicationService.ListEvents(&request) if err != nil { handler.logger.Errorw("error in getting events list", "err", err) @@ -336,10 +415,13 @@ func (handler *K8sApplicationRestHandlerImpl) GetPodLogs(w http.ResponseWriter, podName := vars["podName"] containerName := v.Get("containerName") appId := v.Get("appId") + clusterIdString := v.Get("clusterId") + namespace := v.Get("namespace") /*sinceSeconds, err := strconv.Atoi(v.Get("sinceSeconds")) if err != nil { sinceSeconds = 0 }*/ + token := r.Header.Get("token") follow, err := strconv.ParseBool(v.Get("follow")) if err != nil { follow = false @@ -348,43 +430,80 @@ func (handler *K8sApplicationRestHandlerImpl) GetPodLogs(w http.ResponseWriter, if err != nil { tailLines = 0 } - appIdentifier, err := handler.helmAppService.DecodeAppId(appId) - if err != nil { - handler.logger.Errorw("error in decoding appId", "err", err, "appId", appId) - common.WriteJsonResp(w, err, nil, http.StatusBadRequest) - return - } - request := &ResourceRequestBean{ - AppIdentifier: appIdentifier, - K8sRequest: &application.K8sRequestBean{ - ResourceIdentifier: application.ResourceIdentifier{ - Name: podName, - Namespace: appIdentifier.Namespace, - GroupVersionKind: schema.GroupVersionKind{}, - }, - PodLogsRequest: application.PodLogsRequest{ - //SinceTime: sinceSeconds, - TailLines: tailLines, - Follow: follow, - ContainerName: containerName, + var request *ResourceRequestBean + if appId != "" { + appIdentifier, err := handler.helmAppService.DecodeAppId(appId) + if err != nil { + handler.logger.Errorw("error in decoding appId", "err", err, "appId", appId) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + request = &ResourceRequestBean{ + AppIdentifier: appIdentifier, + ClusterId: appIdentifier.ClusterId, + K8sRequest: &application.K8sRequestBean{ + ResourceIdentifier: application.ResourceIdentifier{ + Name: podName, + Namespace: appIdentifier.Namespace, + GroupVersionKind: schema.GroupVersionKind{}, + }, + PodLogsRequest: application.PodLogsRequest{ + //SinceTime: sinceSeconds, + TailLines: tailLines, + Follow: follow, + ContainerName: containerName, + }, }, - }, - } + } - valid, err := handler.k8sApplicationService.ValidateResourceRequest(request.AppIdentifier, request.K8sRequest) - if err != nil || !valid { - handler.logger.Errorw("error in validating resource request", "err", err) - common.WriteJsonResp(w, err, nil, http.StatusBadRequest) - return - } - // RBAC enforcer applying - rbacObject := handler.enforcerUtil.GetHelmObjectByClusterId(request.AppIdentifier.ClusterId, request.AppIdentifier.Namespace, request.AppIdentifier.ReleaseName) - token := r.Header.Get("token") - if ok := handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionGet, rbacObject); !ok { - common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) + valid, err := handler.k8sApplicationService.ValidateResourceRequest(request.AppIdentifier, request.K8sRequest) + if err != nil || !valid { + handler.logger.Errorw("error in validating resource request", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + // RBAC enforcer applying + rbacObject := handler.enforcerUtilHelm.GetHelmObjectByClusterId(request.AppIdentifier.ClusterId, request.AppIdentifier.Namespace, request.AppIdentifier.ReleaseName) + if ok := handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionGet, rbacObject); !ok { + common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) + return + } + //RBAC enforcer Ends + } else if clusterIdString != "" && namespace != "" { + clusterId, err := strconv.Atoi(clusterIdString) + if err != nil { + handler.logger.Errorw("invalid cluster id", "clusterId", clusterIdString, "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + request = &ResourceRequestBean{ + ClusterId: clusterId, + K8sRequest: &application.K8sRequestBean{ + ResourceIdentifier: application.ResourceIdentifier{ + Name: podName, + Namespace: namespace, + GroupVersionKind: schema.GroupVersionKind{ + Group: "", + Kind: "Pod", + Version: "v1", + }, + }, + PodLogsRequest: application.PodLogsRequest{ + //SinceTime: sinceSeconds, + TailLines: tailLines, + Follow: follow, + ContainerName: containerName, + }, + }, + } + if ok := handler.validateRbac(w, token, *request, casbin.ActionGet); !ok { + return + } + } else { + common.WriteJsonResp(w, errors.New("can not get pod logs as target cluster or namespace is not provided"), nil, http.StatusBadRequest) return } - //RBAC enforcer Ends + lastEventId := r.Header.Get("Last-Event-ID") isReconnect := false if len(lastEventId) > 0 { @@ -406,28 +525,66 @@ func (handler *K8sApplicationRestHandlerImpl) GetPodLogs(w http.ResponseWriter, func (handler *K8sApplicationRestHandlerImpl) GetTerminalSession(w http.ResponseWriter, r *http.Request) { request := &terminal.TerminalSessionRequest{} vars := mux.Vars(r) + token := r.Header.Get("token") request.ContainerName = vars["container"] request.Namespace = vars["namespace"] request.PodName = vars["pod"] request.Shell = vars["shell"] - request.ApplicationId = vars["applicationId"] - - app, err := handler.helmAppService.DecodeAppId(request.ApplicationId) - if err != nil { - handler.logger.Errorw("invalid app id", "err", err) - common.WriteJsonResp(w, err, nil, http.StatusBadRequest) - return + clusterIdString := "" + appId := "" + identifier := vars["identifier"] + if strings.Contains(identifier, "|") { + appId = identifier + } else { + clusterIdString = identifier } - request.ClusterId = app.ClusterId - // RBAC enforcer applying - rbacObject := handler.enforcerUtil.GetHelmObjectByClusterId(app.ClusterId, app.Namespace, app.ReleaseName) - token := r.Header.Get("token") - if ok := handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionGet, rbacObject); !ok { - common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) + if appId != "" { + request.ApplicationId = appId + app, err := handler.helmAppService.DecodeAppId(request.ApplicationId) + if err != nil { + handler.logger.Errorw("invalid app id", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + request.ClusterId = app.ClusterId + + // RBAC enforcer applying + rbacObject := handler.enforcerUtilHelm.GetHelmObjectByClusterId(app.ClusterId, app.Namespace, app.ReleaseName) + if ok := handler.enforcer.Enforce(token, casbin.ResourceHelmApp, casbin.ActionGet, rbacObject); !ok { + common.WriteJsonResp(w, errors2.New("unauthorized"), nil, http.StatusForbidden) + return + } + //RBAC enforcer Ends + } else if clusterIdString != "" { + clusterId, err := strconv.Atoi(clusterIdString) + if err != nil { + handler.logger.Errorw("invalid cluster id", "clusterId", clusterIdString, "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + request.ClusterId = clusterId + resourceRequestBean := ResourceRequestBean{ + ClusterId: clusterId, + K8sRequest: &application.K8sRequestBean{ + ResourceIdentifier: application.ResourceIdentifier{ + Name: request.PodName, + Namespace: request.Namespace, + GroupVersionKind: schema.GroupVersionKind{ + Group: "", + Kind: "Pod", + Version: "v1", + }, + }, + }, + } + if ok := handler.validateRbac(w, token, resourceRequestBean, casbin.ActionUpdate); !ok { + return + } + } else { + common.WriteJsonResp(w, errors.New("can not get terminal session as target cluster is not provided"), nil, http.StatusBadRequest) return } - //RBAC enforcer Ends status, message, err := handler.terminalSessionHandler.GetTerminalSession(request) common.WriteJsonResp(w, err, message, status) @@ -450,3 +607,91 @@ func (handler *K8sApplicationRestHandlerImpl) GetResourceInfo(w http.ResponseWri common.WriteJsonResp(w, nil, response, http.StatusOK) return } + +func (handler *K8sApplicationRestHandlerImpl) GetAllApiResources(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 + } + + // get clusterId from request + vars := mux.Vars(r) + clusterId, err := strconv.Atoi(vars["clusterId"]) + if err != nil { + handler.logger.Errorw("request err in getting clusterId in GetAllApiResources", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + + isSuperAdmin := false + token := r.Header.Get("token") + if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); ok { + isSuperAdmin = true + } + + // get data from service + response, err := handler.k8sApplicationService.GetAllApiResources(clusterId, isSuperAdmin, userId) + if err != nil { + handler.logger.Errorw("error in getting api-resources", "clusterId", clusterId, "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + + // send unauthorised if response is empty + if !isSuperAdmin && (response == nil || len(response.ApiResources) == 0) { + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + return + } + + common.WriteJsonResp(w, nil, response, http.StatusOK) +} + +func (handler *K8sApplicationRestHandlerImpl) GetResourceList(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + token := r.Header.Get("token") + var request ResourceRequestBean + err := decoder.Decode(&request) + if err != nil { + handler.logger.Errorw("error in decoding request body", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + response, err := handler.k8sApplicationService.GetResourceList(token, &request, handler.verifyRbacForCluster) + if err != nil { + handler.logger.Errorw("error in getting resource list", "err", err) + if statusErr, ok := err.(*errors3.StatusError); ok && statusErr.Status().Code == 404 { + err = &util2.ApiError{Code: "404", HttpStatusCode: 404, UserMessage: "no resource found", InternalMessage: err.Error()} + } + common.WriteJsonResp(w, err, response, http.StatusInternalServerError) + return + } + common.WriteJsonResp(w, nil, response, http.StatusOK) +} + +func (handler *K8sApplicationRestHandlerImpl) ApplyResources(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + var request application.ApplyResourcesRequest + token := r.Header.Get("token") + err := decoder.Decode(&request) + if err != nil { + handler.logger.Errorw("error in decoding request body", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + + response, err := handler.k8sApplicationService.ApplyResources(token, &request, handler.verifyRbacForCluster) + if err != nil { + handler.logger.Errorw("error in applying resource", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + common.WriteJsonResp(w, nil, response, http.StatusOK) +} + +func (handler *K8sApplicationRestHandlerImpl) verifyRbacForCluster(token string, clusterName string, request ResourceRequestBean, casbinAction string) bool { + k8sRequest := request.K8sRequest + resourceIdentifier := k8sRequest.ResourceIdentifier + resourceName, objectName := handler.enforcerUtil.GetRBACNameForClusterEntity(clusterName, resourceIdentifier) + return handler.enforcer.Enforce(token, resourceName, casbinAction, strings.ToLower(objectName)) +} diff --git a/util/k8s/k8sApplicationRouter.go b/util/k8s/k8sApplicationRouter.go index c81b2cd714..aad3a6881a 100644 --- a/util/k8s/k8sApplicationRouter.go +++ b/util/k8s/k8sApplicationRouter.go @@ -39,13 +39,15 @@ func (impl *K8sApplicationRouterImpl) InitK8sApplicationRouter(k8sAppRouter *mux HandlerFunc(impl.k8sApplicationRestHandler.ListEvents).Methods("POST") k8sAppRouter.Path("/pods/logs/{podName}"). - Queries("containerName", "{containerName}", "appId", "{appId}"). + Queries("containerName", "{containerName}"). + //Queries("containerName", "{containerName}", "appId", "{appId}"). + //Queries("clusterId", "{clusterId}", "namespace", "${namespace}"). //Queries("sinceSeconds", "{sinceSeconds}"). Queries("follow", "{follow}"). Queries("tailLines", "{tailLines}"). HandlerFunc(impl.k8sApplicationRestHandler.GetPodLogs).Methods("GET") - k8sAppRouter.Path("/pod/exec/session/{applicationId}/{namespace}/{pod}/{shell}/{container}"). + k8sAppRouter.Path("/pod/exec/session/{identifier}/{namespace}/{pod}/{shell}/{container}"). HandlerFunc(impl.k8sApplicationRestHandler.GetTerminalSession).Methods("GET") k8sAppRouter.PathPrefix("/pod/exec/sockjs/ws").Handler(terminal.CreateAttachHandler("/pod/exec/sockjs/ws")) @@ -54,4 +56,13 @@ func (impl *K8sApplicationRouterImpl) InitK8sApplicationRouter(k8sAppRouter *mux k8sAppRouter.Path("/resource/inception/info"). HandlerFunc(impl.k8sApplicationRestHandler.GetResourceInfo).Methods("GET") + + k8sAppRouter.Path("/api-resources/{clusterId}"). + HandlerFunc(impl.k8sApplicationRestHandler.GetAllApiResources).Methods("GET") + + k8sAppRouter.Path("/resource/list"). + HandlerFunc(impl.k8sApplicationRestHandler.GetResourceList).Methods("POST") + + k8sAppRouter.Path("/resources/apply"). + HandlerFunc(impl.k8sApplicationRestHandler.ApplyResources).Methods("POST") } diff --git a/util/k8s/k8sApplicationService.go b/util/k8s/k8sApplicationService.go index c7694028d4..8718f9a114 100644 --- a/util/k8s/k8sApplicationService.go +++ b/util/k8s/k8sApplicationService.go @@ -2,6 +2,7 @@ package k8s import ( "context" + "encoding/json" "fmt" "github.com/caarlos0/env" "github.com/devtron-labs/devtron/api/bean" @@ -11,9 +12,14 @@ import ( "github.com/devtron-labs/devtron/client/k8s/application" "github.com/devtron-labs/devtron/internal/util" "github.com/devtron-labs/devtron/pkg/cluster" + "github.com/devtron-labs/devtron/pkg/user/casbin" util3 "github.com/devtron-labs/devtron/pkg/util" + yamlUtil "github.com/devtron-labs/devtron/util/yaml" "go.uber.org/zap" "io" + errors2 "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/rest" "strconv" @@ -40,7 +46,11 @@ type K8sApplicationService interface { GetManifestsByBatch(ctx context.Context, request []ResourceRequestBean) ([]BatchResourceResponse, error) FilterServiceAndIngress(resourceTreeInf map[string]interface{}, validRequests []ResourceRequestBean, appDetail bean.AppDetailContainer, appId string) []ResourceRequestBean GetUrlsByBatch(resp []BatchResourceResponse) []interface{} + GetAllApiResources(clusterId int, isSuperAdmin bool, userId int32) (*application.GetAllApiResourcesResponse, error) + GetResourceList(token string, request *ResourceRequestBean, validateResourceAccess func(token string, clusterName string, request ResourceRequestBean, casbinAction string) bool) (*application.ClusterResourceListMap, error) + ApplyResources(token string, request *application.ApplyResourcesRequest, resourceRbacHandler func(token string, clusterName string, request ResourceRequestBean, casbinAction string) bool) ([]*application.ApplyResourcesResponse, error) } + type K8sApplicationServiceImpl struct { logger *zap.SugaredLogger clusterService cluster.ClusterService @@ -82,6 +92,7 @@ type ResourceRequestBean struct { AppId string `json:"appId"` AppIdentifier *client.AppIdentifier `json:"-"` K8sRequest *application.K8sRequestBean `json:"k8sRequest"` + ClusterId int `json:"clusterId"` // clusterId is used when request is for direct cluster (not for helm release) } type ResourceInfo struct { @@ -283,10 +294,11 @@ func (impl *K8sApplicationServiceImpl) getManifestsByBatch(requests []ResourceRe } func (impl *K8sApplicationServiceImpl) GetResource(request *ResourceRequestBean) (*application.ManifestResponse, error) { + clusterId := request.ClusterId //getting rest config by clusterId - restConfig, err := impl.GetRestConfigByClusterId(request.AppIdentifier.ClusterId) + restConfig, err := impl.GetRestConfigByClusterId(clusterId) if err != nil { - impl.logger.Errorw("error in getting rest config by cluster Id", "err", err, "clusterId", request.AppIdentifier.ClusterId) + impl.logger.Errorw("error in getting rest config by cluster Id", "err", err, "clusterId", clusterId) return nil, err } resp, err := impl.k8sClientService.GetResource(restConfig, request.K8sRequest) @@ -332,9 +344,10 @@ func (impl *K8sApplicationServiceImpl) CreateResource(request *ResourceRequestBe func (impl *K8sApplicationServiceImpl) UpdateResource(request *ResourceRequestBean) (*application.ManifestResponse, error) { //getting rest config by clusterId - restConfig, err := impl.GetRestConfigByClusterId(request.AppIdentifier.ClusterId) + clusterId := request.ClusterId + restConfig, err := impl.GetRestConfigByClusterId(clusterId) if err != nil { - impl.logger.Errorw("error in getting rest config by cluster Id", "err", err, "clusterId", request.AppIdentifier.ClusterId) + impl.logger.Errorw("error in getting rest config by cluster Id", "err", err, "clusterId", clusterId) return nil, err } resp, err := impl.k8sClientService.UpdateResource(restConfig, request.K8sRequest) @@ -347,9 +360,10 @@ func (impl *K8sApplicationServiceImpl) UpdateResource(request *ResourceRequestBe func (impl *K8sApplicationServiceImpl) DeleteResource(request *ResourceRequestBean) (*application.ManifestResponse, error) { //getting rest config by clusterId - restConfig, err := impl.GetRestConfigByClusterId(request.AppIdentifier.ClusterId) + clusterId := request.ClusterId + restConfig, err := impl.GetRestConfigByClusterId(clusterId) if err != nil { - impl.logger.Errorw("error in getting rest config by cluster Id", "err", err, "clusterId", request.AppIdentifier.ClusterId) + impl.logger.Errorw("error in getting rest config by cluster Id", "err", err, "clusterId", clusterId) return nil, err } resp, err := impl.k8sClientService.DeleteResource(restConfig, request.K8sRequest) @@ -361,10 +375,11 @@ func (impl *K8sApplicationServiceImpl) DeleteResource(request *ResourceRequestBe } func (impl *K8sApplicationServiceImpl) ListEvents(request *ResourceRequestBean) (*application.EventsResponse, error) { + clusterId := request.ClusterId //getting rest config by clusterId - restConfig, err := impl.GetRestConfigByClusterId(request.AppIdentifier.ClusterId) + restConfig, err := impl.GetRestConfigByClusterId(clusterId) if err != nil { - impl.logger.Errorw("error in getting rest config by cluster Id", "err", err, "clusterId", request.AppIdentifier.ClusterId) + impl.logger.Errorw("error in getting rest config by cluster Id", "err", err, "clusterId", clusterId) return nil, err } resp, err := impl.k8sClientService.ListEvents(restConfig, request.K8sRequest) @@ -376,10 +391,11 @@ func (impl *K8sApplicationServiceImpl) ListEvents(request *ResourceRequestBean) } func (impl *K8sApplicationServiceImpl) GetPodLogs(request *ResourceRequestBean) (io.ReadCloser, error) { + clusterId := request.ClusterId //getting rest config by clusterId - restConfig, err := impl.GetRestConfigByClusterId(request.AppIdentifier.ClusterId) + restConfig, err := impl.GetRestConfigByClusterId(clusterId) if err != nil { - impl.logger.Errorw("error in getting rest config by cluster Id", "err", err, "clusterId", request.AppIdentifier.ClusterId) + impl.logger.Errorw("error in getting rest config by cluster Id", "err", err, "clusterId", clusterId) return nil, err } resp, err := impl.k8sClientService.GetPodLogs(restConfig, request.K8sRequest) @@ -400,7 +416,7 @@ func (impl *K8sApplicationServiceImpl) GetRestConfigByClusterId(clusterId int) ( bearerToken := configMap["bearer_token"] var restConfig *rest.Config if cluster.ClusterName == DEFAULT_CLUSTER && len(bearerToken) == 0 { - restConfig, err = rest.InClusterConfig() + restConfig, err = impl.K8sUtil.GetK8sClusterRestConfig() if err != nil { impl.logger.Errorw("error in getting rest config for default cluster", "err", err) return nil, err @@ -417,7 +433,7 @@ func (impl *K8sApplicationServiceImpl) GetRestConfigByCluster(cluster *cluster.C var restConfig *rest.Config var err error if cluster.ClusterName == DEFAULT_CLUSTER && len(bearerToken) == 0 { - restConfig, err = rest.InClusterConfig() + restConfig, err = impl.K8sUtil.GetK8sClusterRestConfig() if err != nil { impl.logger.Errorw("error in getting rest config for default cluster", "err", err) return nil, err @@ -474,3 +490,229 @@ func (impl *K8sApplicationServiceImpl) GetResourceInfo() (*ResourceInfo, error) response := &ResourceInfo{PodName: pod.Name} return response, nil } + +func (impl *K8sApplicationServiceImpl) GetAllApiResources(clusterId int, isSuperAdmin bool, userId int32) (*application.GetAllApiResourcesResponse, error) { + impl.logger.Infow("getting all api-resources", "clusterId", clusterId) + restConfig, err := impl.GetRestConfigByClusterId(clusterId) + if err != nil { + impl.logger.Errorw("error in getting cluster rest config", "clusterId", clusterId, "err", err) + return nil, err + } + allApiResources, err := impl.k8sClientService.GetApiResources(restConfig, LIST_VERB) + if err != nil { + return nil, err + } + + // FILTER STARTS + // 1) remove ""/v1 event kind if event kind exist in events.k8s.io/v1 and ""/v1 + k8sEventIndex := -1 + v1EventIndex := -1 + for index, apiResource := range allApiResources { + gvk := apiResource.Gvk + if gvk.Kind == EVENT_K8S_KIND && gvk.Version == "v1" { + if gvk.Group == "" { + v1EventIndex = index + } else if gvk.Group == "events.k8s.io" { + k8sEventIndex = index + } + } + } + if k8sEventIndex > -1 && v1EventIndex > -1 { + allApiResources = append(allApiResources[:v1EventIndex], allApiResources[v1EventIndex+1:]...) + } + // FILTER ENDS + + // RBAC FILER STARTS + allowedAll := isSuperAdmin + filteredApiResources := make([]*application.K8sApiResource, 0) + if !isSuperAdmin { + clusterBean, err := impl.clusterService.FindById(clusterId) + if err != nil { + impl.logger.Errorw("failed to find cluster for id", "err", err, "clusterId", clusterId) + return nil, err + } + roles, err := impl.clusterService.FetchRolesFromGroup(userId) + if err != nil { + impl.logger.Errorw("error on fetching user roles for cluster list", "err", err) + return nil, err + } + + allowedGroupKinds := make(map[string]bool) // group||kind + for _, role := range roles { + if clusterBean.ClusterName != role.Cluster { + continue + } + if role.Group == "" && role.Kind == "" { + allowedAll = true + break + } + groupName := role.Group + if groupName == "" { + groupName = "*" + } else if groupName == casbin.ClusterEmptyGroupPlaceholder { + groupName = "" + } + allowedGroupKinds[groupName+"||"+role.Kind] = true + } + + if !allowedAll { + for _, apiResource := range allApiResources { + gvk := apiResource.Gvk + _, found := allowedGroupKinds[gvk.Group+"||"+gvk.Kind] + if found { + filteredApiResources = append(filteredApiResources, apiResource) + } else { + _, found = allowedGroupKinds["*"+"||"+gvk.Kind] + if found { + filteredApiResources = append(filteredApiResources, apiResource) + } + } + } + } + } + response := &application.GetAllApiResourcesResponse{ + AllowedAll: allowedAll, + } + if allowedAll { + response.ApiResources = allApiResources + } else { + response.ApiResources = filteredApiResources + } + // RBAC FILER ENDS + + return response, nil +} + +func (impl *K8sApplicationServiceImpl) GetResourceList(token string, request *ResourceRequestBean, validateResourceAccess func(token string, clusterName string, request ResourceRequestBean, casbinAction string) bool) (*application.ClusterResourceListMap, error) { + resourceList := &application.ClusterResourceListMap{} + clusterId := request.ClusterId + clusterBean, err := impl.clusterService.FindById(clusterId) + if err != nil { + impl.logger.Errorw("error in getting cluster by cluster Id", "err", err, "clusterId", clusterId) + return resourceList, err + } + restConfig, err := impl.GetRestConfigByCluster(clusterBean) + if err != nil { + impl.logger.Errorw("error in getting rest config by cluster Id", "err", err, "clusterId", request.ClusterId) + return resourceList, err + } + k8sRequest := request.K8sRequest + resp, namespaced, err := impl.k8sClientService.GetResourceList(restConfig, k8sRequest) + if err != nil { + impl.logger.Errorw("error in getting resource list", "err", err, "request", request) + return resourceList, err + } + checkForResourceCallback := func(namespace, resourceName string) bool { + resourceIdentifier := k8sRequest.ResourceIdentifier + resourceIdentifier.Name = resourceName + resourceIdentifier.Namespace = namespace + k8sRequest.ResourceIdentifier = resourceIdentifier + return validateResourceAccess(token, clusterBean.ClusterName, *request, casbin.ActionGet) + } + resourceList, err = impl.K8sUtil.BuildK8sObjectListTableData(&resp.Resources, namespaced, request.K8sRequest.ResourceIdentifier.GroupVersionKind.Kind, checkForResourceCallback) + if err != nil { + impl.logger.Errorw("error on parsing for k8s resource", "err", err) + return resourceList, err + } + return resourceList, nil +} + +func (impl *K8sApplicationServiceImpl) ApplyResources(token string, request *application.ApplyResourcesRequest, validateResourceAccess func(token string, clusterName string, request ResourceRequestBean, casbinAction string) bool) ([]*application.ApplyResourcesResponse, error) { + manifests, err := yamlUtil.SplitYAMLs([]byte(request.Manifest)) + if err != nil { + impl.logger.Errorw("error in splitting yaml in manifest", "err", err) + return nil, err + } + + //getting rest config by clusterId + clusterId := request.ClusterId + clusterBean, err := impl.clusterService.FindById(clusterId) + if err != nil { + impl.logger.Errorw("error in getting clusterBean by cluster Id", "clusterId", clusterId, "err", err) + return nil, err + } + restConfig, err := impl.GetRestConfigByCluster(clusterBean) + if err != nil { + impl.logger.Errorw("error in getting rest config by cluster", "clusterId", clusterId, "err", err) + return nil, err + } + + var response []*application.ApplyResourcesResponse + for _, manifest := range manifests { + var namespace string + manifestNamespace := manifest.GetNamespace() + if len(manifestNamespace) > 0 { + namespace = manifestNamespace + } else { + namespace = DEFAULT_NAMESPACE + } + manifestRes := &application.ApplyResourcesResponse{ + Name: manifest.GetName(), + Kind: manifest.GetKind(), + } + resourceRequestBean := ResourceRequestBean{ + ClusterId: clusterId, + K8sRequest: &application.K8sRequestBean{ + ResourceIdentifier: application.ResourceIdentifier{ + Name: manifest.GetName(), + Namespace: namespace, + GroupVersionKind: manifest.GroupVersionKind(), + }, + }, + } + actionAllowed := validateResourceAccess(token, clusterBean.ClusterName, resourceRequestBean, casbin.ActionUpdate) + if actionAllowed { + resourceExists, err := impl.applyResourceFromManifest(manifest, restConfig, namespace) + manifestRes.IsUpdate = resourceExists + if err != nil { + manifestRes.Error = err.Error() + } + } else { + manifestRes.Error = "permission-denied" + } + response = append(response, manifestRes) + } + + return response, nil +} + +func (impl *K8sApplicationServiceImpl) applyResourceFromManifest(manifest unstructured.Unstructured, restConfig *rest.Config, namespace string) (bool, error) { + var isUpdateResource bool + k8sRequestBean := &application.K8sRequestBean{ + ResourceIdentifier: application.ResourceIdentifier{ + Name: manifest.GetName(), + Namespace: namespace, + GroupVersionKind: manifest.GroupVersionKind(), + }, + } + jsonStrByteErr, err := json.Marshal(manifest.UnstructuredContent()) + if err != nil { + impl.logger.Errorw("error in marshalling json", "err", err) + return isUpdateResource, err + } + jsonStr := string(jsonStrByteErr) + _, err = impl.k8sClientService.GetResource(restConfig, k8sRequestBean) + if err != nil { + statusError, ok := err.(*errors2.StatusError) + if !ok || statusError == nil || statusError.ErrStatus.Reason != metav1.StatusReasonNotFound { + impl.logger.Errorw("error in getting resource", "err", err) + return isUpdateResource, err + } + // case of resource not found + _, err = impl.k8sClientService.CreateResource(restConfig, k8sRequestBean, jsonStr) + if err != nil { + impl.logger.Errorw("error in creating resource", "err", err) + return isUpdateResource, err + } + } else { + // case of resource update + isUpdateResource = true + _, err = impl.k8sClientService.ApplyResource(restConfig, k8sRequestBean, jsonStr) + if err != nil { + impl.logger.Errorw("error in updating resource", "err", err) + return isUpdateResource, err + } + } + + return isUpdateResource, nil +} diff --git a/util/rbac/EnforcerUtil.go b/util/rbac/EnforcerUtil.go index 270a9acf92..6bff4e8e05 100644 --- a/util/rbac/EnforcerUtil.go +++ b/util/rbac/EnforcerUtil.go @@ -19,10 +19,12 @@ package rbac import ( "fmt" + "github.com/devtron-labs/devtron/client/k8s/application" "github.com/devtron-labs/devtron/internal/sql/repository/app" "github.com/devtron-labs/devtron/internal/sql/repository/pipelineConfig" "github.com/devtron-labs/devtron/pkg/cluster/repository" "github.com/devtron-labs/devtron/pkg/team" + "github.com/devtron-labs/devtron/pkg/user/casbin" "go.uber.org/zap" "strings" ) @@ -47,6 +49,7 @@ type EnforcerUtil interface { GetHelmObjectByProjectIdAndEnvId(teamId int, envId int) (string, string) GetEnvRBACNameByCdPipelineIdAndEnvId(cdPipelineId int, envId int) string GetAppRBACNameByTeamIdAndAppId(teamId int, appId int) string + GetRBACNameForClusterEntity(clusterName string, resourceIdentifier application.ResourceIdentifier) (resourceName, objectName string) } type EnforcerUtilImpl struct { logger *zap.SugaredLogger @@ -416,3 +419,20 @@ func (impl EnforcerUtilImpl) GetAppRBACNameByTeamIdAndAppId(teamId int, appId in } return fmt.Sprintf("%s/%s", strings.ToLower(team.Name), strings.ToLower(application.AppName)) } + +func (impl EnforcerUtilImpl) GetRBACNameForClusterEntity(clusterName string, resourceIdentifier application.ResourceIdentifier) (resourceName, objectName string) { + namespace := resourceIdentifier.Namespace + objectName = resourceIdentifier.Name + groupVersionKind := resourceIdentifier.GroupVersionKind + groupName := groupVersionKind.Group + kindName := groupVersionKind.Kind + if groupName == "" { + groupName = casbin.ClusterEmptyGroupPlaceholder + } + if namespace == "" { //empty value means all namespace access would occur for non-namespace resources + namespace = "*" + } + resourceName = fmt.Sprintf(casbin.ClusterResourceRegex, clusterName, namespace) + objectName = fmt.Sprintf(casbin.ClusterObjectRegex, groupName, kindName, objectName) + return resourceName, objectName +} diff --git a/wire_gen.go b/wire_gen.go index 3444533821..090f3abade 100644 --- a/wire_gen.go +++ b/wire_gen.go @@ -1,8 +1,7 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate go run github.com/google/wire/cmd/wire -//go:build !wireinject -// +build !wireinject +//go:generate wire +//+build !wireinject package main @@ -199,7 +198,12 @@ func InitializeApp() (*App, error) { v := informer.NewGlobalMapClusterNamespace() k8sInformerFactoryImpl := informer.NewK8sInformerFactoryImpl(sugaredLogger, v, runtimeConfig) gitOpsConfigRepositoryImpl := repository.NewGitOpsConfigRepositoryImpl(sugaredLogger, db) - clusterServiceImplExtended := cluster2.NewClusterServiceImplExtended(clusterRepositoryImpl, environmentRepositoryImpl, grafanaClientImpl, sugaredLogger, installedAppRepositoryImpl, k8sUtil, serviceClientImpl, k8sInformerFactoryImpl, gitOpsConfigRepositoryImpl) + defaultAuthPolicyRepositoryImpl := repository4.NewDefaultAuthPolicyRepositoryImpl(db, sugaredLogger) + defaultAuthRoleRepositoryImpl := repository4.NewDefaultAuthRoleRepositoryImpl(db, sugaredLogger) + userAuthRepositoryImpl := repository4.NewUserAuthRepositoryImpl(db, sugaredLogger, defaultAuthPolicyRepositoryImpl, defaultAuthRoleRepositoryImpl) + userRepositoryImpl := repository4.NewUserRepositoryImpl(db, sugaredLogger) + roleGroupRepositoryImpl := repository4.NewRoleGroupRepositoryImpl(db, sugaredLogger) + clusterServiceImplExtended := cluster2.NewClusterServiceImplExtended(clusterRepositoryImpl, environmentRepositoryImpl, grafanaClientImpl, sugaredLogger, installedAppRepositoryImpl, k8sUtil, serviceClientImpl, k8sInformerFactoryImpl, gitOpsConfigRepositoryImpl, userAuthRepositoryImpl, userRepositoryImpl, roleGroupRepositoryImpl) helmClientConfig, err := client3.GetConfig() if err != nil { return nil, err @@ -209,9 +213,6 @@ func InitializeApp() (*App, error) { enforcerUtilHelmImpl := rbac.NewEnforcerUtilHelmImpl(sugaredLogger, clusterRepositoryImpl) serverDataStoreServerDataStore := serverDataStore.InitServerDataStore() appStoreApplicationVersionRepositoryImpl := appStoreDiscoverRepository.NewAppStoreApplicationVersionRepositoryImpl(sugaredLogger, db) - defaultAuthPolicyRepositoryImpl := repository4.NewDefaultAuthPolicyRepositoryImpl(db, sugaredLogger) - defaultAuthRoleRepositoryImpl := repository4.NewDefaultAuthRoleRepositoryImpl(db, sugaredLogger) - userAuthRepositoryImpl := repository4.NewUserAuthRepositoryImpl(db, sugaredLogger, defaultAuthPolicyRepositoryImpl, defaultAuthRoleRepositoryImpl) k8sClient, err := client2.NewK8sClient(runtimeConfig) if err != nil { return nil, err @@ -227,8 +228,6 @@ func InitializeApp() (*App, error) { apiTokenSecretStore := apiTokenAuth.InitApiTokenSecretStore() sessionManager := middleware.NewSessionManager(settings, dexConfig, apiTokenSecretStore) loginService := middleware.NewUserLogin(sessionManager, k8sClient) - userRepositoryImpl := repository4.NewUserRepositoryImpl(db, sugaredLogger) - roleGroupRepositoryImpl := repository4.NewRoleGroupRepositoryImpl(db, sugaredLogger) userCommonServiceImpl := user.NewUserCommonServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, sessionManager) userAuditRepositoryImpl := repository4.NewUserAuditRepositoryImpl(db) userAuditServiceImpl := user.NewUserAuditServiceImpl(sugaredLogger, userAuditRepositoryImpl) @@ -517,7 +516,7 @@ func InitializeApp() (*App, error) { workflowStatusUpdateHandlerImpl := pubsub2.NewWorkflowStatusUpdateHandlerImpl(sugaredLogger, pubSubClient, ciHandlerImpl, cdHandlerImpl, eventSimpleFactoryImpl, eventRESTClientImpl, cdWorkflowRepositoryImpl) applicationStatusUpdateHandlerImpl := pubsub2.NewApplicationStatusUpdateHandlerImpl(sugaredLogger, pubSubClient, appServiceImpl, workflowDagExecutorImpl, installedAppServiceImpl) roleGroupServiceImpl := user.NewRoleGroupServiceImpl(userAuthRepositoryImpl, sugaredLogger, userRepositoryImpl, roleGroupRepositoryImpl, userCommonServiceImpl) - userRestHandlerImpl := user2.NewUserRestHandlerImpl(userServiceImpl, validate, sugaredLogger, enforcerImpl, roleGroupServiceImpl) + userRestHandlerImpl := user2.NewUserRestHandlerImpl(userServiceImpl, validate, sugaredLogger, enforcerImpl, roleGroupServiceImpl, userCommonServiceImpl) userRouterImpl := user2.NewUserRouterImpl(userRestHandlerImpl) chartRefRestHandlerImpl := restHandler.NewChartRefRestHandlerImpl(chartServiceImpl, sugaredLogger) chartRefRouterImpl := router.NewChartRefRouterImpl(chartRefRestHandlerImpl) @@ -614,7 +613,7 @@ func InitializeApp() (*App, error) { coreAppRouterImpl := router.NewCoreAppRouterImpl(coreAppRestHandlerImpl) helmAppRestHandlerImpl := client3.NewHelmAppRestHandlerImpl(sugaredLogger, helmAppServiceImpl, enforcerImpl, clusterServiceImplExtended, enforcerUtilHelmImpl, appStoreDeploymentCommonServiceImpl, userServiceImpl, attributesServiceImpl, serverEnvConfigServerEnvConfig) helmAppRouterImpl := client3.NewHelmAppRouterImpl(helmAppRestHandlerImpl) - k8sApplicationRestHandlerImpl := k8s.NewK8sApplicationRestHandlerImpl(sugaredLogger, k8sApplicationServiceImpl, pumpImpl, terminalSessionHandlerImpl, enforcerImpl, enforcerUtilHelmImpl, clusterServiceImplExtended, helmAppServiceImpl, userServiceImpl) + k8sApplicationRestHandlerImpl := k8s.NewK8sApplicationRestHandlerImpl(sugaredLogger, k8sApplicationServiceImpl, pumpImpl, terminalSessionHandlerImpl, enforcerImpl, enforcerUtilHelmImpl, enforcerUtilImpl, clusterServiceImplExtended, helmAppServiceImpl, userServiceImpl) k8sApplicationRouterImpl := k8s.NewK8sApplicationRouterImpl(k8sApplicationRestHandlerImpl) pProfRestHandlerImpl := restHandler.NewPProfRestHandler(userServiceImpl) pProfRouterImpl := router.NewPProfRouter(sugaredLogger, pProfRestHandlerImpl)