package resolver import ( "context" "errors" "fmt" "reflect" "edge-infra.dev/pkg/edge/api/apierror" "edge-infra.dev/pkg/edge/api/graph/model" "edge-infra.dev/pkg/edge/api/middleware" "edge-infra.dev/pkg/edge/api/utils" "edge-infra.dev/pkg/edge/bsl" "github.com/99designs/gqlgen/graphql" ) func UserHasMatchingRole(ctx context.Context, apiRoles []model.Role) bool { u := middleware.ForContext(ctx) if u == nil || len(u.Roles) == 0 { return false } for _, role := range apiRoles { if utils.Contains(u.Roles, string(role)) { return true } } return false } func (r *Resolver) CanAssignRole() func(ctx context.Context, obj interface{}, next graphql.Resolver, abilityMap map[string]interface{}) (res interface{}, err error) { return func(ctx context.Context, obj interface{}, next graphql.Resolver, abilityMap map[string]interface{}) (res interface{}, err error) { userRoles, err := middleware.GetEdgeRoles(ctx) if err != nil { return nil, err } bm, ok := obj.(map[string]interface{}) if !ok { return nil, fmt.Errorf("input object is not a map") } requestedRoles, ok := bm["roles"] // assign roles requestedRole, okRole := bm["role"].(string) // assign role if !ok { if !okRole { return nil, fmt.Errorf("there is no role field in the input map") } } accessibleRoles := []string{} for _, userRole := range userRoles { if abilityMap[userRole] != nil { for _, role := range abilityMap[userRole].([]interface{}) { accessibleRoles = append(accessibleRoles, role.(string)) } } } // For assign role if requestedRole != "" { if utils.Contains(accessibleRoles, requestedRole) { return next(ctx) } return nil, fmt.Errorf("access denied: user cannot assign the requested role %s", requestedRole) } // Assign roles // Get user being assigned roles userToAssign, okUser := bm["username"].(string) if !okUser { return nil, fmt.Errorf("fail to grab user to assign info") } userCurrentRoles, err := r.RoleService.GetEdgeGroupsForUserUser(ctx, userToAssign) if err != nil { return nil, fmt.Errorf("fail to get role for current user being assigned") } if rqRole, okRq := requestedRoles.([]interface{}); okRq { for _, roleValue := range rqRole { role := roleValue.(string) if role == "" { return nil, fmt.Errorf("empty role input") } if !utils.Contains(accessibleRoles, role) && !utils.Contains(userCurrentRoles, role) { // Check if user being assiged already has role(s) in the requested role list return nil, fmt.Errorf("access denied: user cannot assign the requested role %s", role) } } } return next(ctx) } } func (r *Resolver) HasHelmWorkloadAccess() func(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (res interface{}, err error) { return func(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (res interface{}, err error) { if UserHasMatchingRole(ctx, []model.Role{model.RoleTotpRole}) { return next(ctx) } if UserHasMatchingRole(ctx, []model.Role{model.RoleEdgeBootstrap}) { return next(ctx) } id, ok, o := getUserOrg(ctx, obj, field) if !ok || id == "" { return next(ctx) } banners, errs := r.BannerService.GetBanners(ctx) if errs != nil && errs.Error() != apierror.BannerCheckMessage { return nil, apierror.AddEmptyAPIErrorToResponse(ctx, errs) } banner, errs := r.HelmService.GetBannerByHelmEdgeID(ctx, id) if errs != nil { return nil, apierror.AddEmptyAPIErrorToResponse(ctx, errs) } t, err := r.TenantService.GetTenantByBannerID(ctx, banner.BannerEdgeID) if err != nil || o != t.OrgName { return nil, fmt.Errorf("access denied: user org %s does not have access to requested organization", o) } if !isUserAssignedToBanner(banners, banner.BannerEdgeID) { return nil, fmt.Errorf("access denied: user does not have access to the the requested workload resource %s", id) } addIDsToContext(ctx, t.TenantEdgeID, banner.BannerEdgeID, "") return next(ctx) } } func (r *Resolver) HasClusterAccess() func(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (res interface{}, err error) { return func(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (res interface{}, err error) { if UserHasMatchingRole(ctx, []model.Role{model.RoleTotpRole}) { return next(ctx) } if UserHasMatchingRole(ctx, []model.Role{model.RoleEdgeBootstrap}) { return next(ctx) } id, ok, o := getUserOrg(ctx, obj, field) if !ok || id == "" { return next(ctx) } banners, errs := r.BannerService.GetBanners(ctx) if errs != nil && errs.Error() != apierror.BannerCheckMessage { return nil, apierror.AddEmptyAPIErrorToResponse(ctx, errs) } cluster, errs := r.StoreClusterService.GetCluster(ctx, id) if errs != nil { return nil, apierror.AddEmptyAPIErrorToResponse(ctx, errs) } t, err := r.TenantService.GetTenantByBannerID(ctx, cluster.BannerEdgeID) if err != nil || o != t.OrgName { return nil, fmt.Errorf("access denied: user org %s does not have access to requested organization", o) } if !isUserAssignedToBanner(banners, cluster.BannerEdgeID) { return nil, fmt.Errorf("access denied: user does not have access to the banner %s that the requested cluster resource %s exists in", cluster.BannerEdgeID, id) } addIDsToContext(ctx, t.TenantEdgeID, cluster.BannerEdgeID, cluster.ClusterEdgeID) return next(ctx) } } func addIDsToContext(ctx context.Context, tenantID string, bannerID string, clusterEdgeID string) { user := middleware.ForContext(ctx) user.ClusterEdgeID = clusterEdgeID user.BannerID = bannerID user.TenantID = tenantID } func fullUsername(username, org string) string { return fmt.Sprintf("acct:%s@%s", org, username) } func (r *Resolver) HasBannerAccess() func(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (res interface{}, err error) { return func(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (res interface{}, err error) { if UserHasMatchingRole(ctx, []model.Role{model.RoleTotpRole}) { return next(ctx) } id, ok, o := getUserOrg(ctx, obj, field) if !ok || id == "" { return next(ctx) } t, err := r.TenantService.GetTenantByBannerID(ctx, id) if err != nil || o != t.OrgName { return nil, fmt.Errorf("access denied: user org %s does not have access to requested organization", o) } banners, errs := r.BannerService.GetBanners(ctx) if errs != nil && errs.Error() != apierror.BannerCheckMessage { return nil, apierror.AddAPIErrorToResponse(ctx, err) } if !isUserAssignedToBanner(banners, id) { return nil, fmt.Errorf("access denied: user does not have access to requested banner resource %s", id) } addIDsToContext(ctx, t.TenantEdgeID, id, "") return next(ctx) } } func (r *Resolver) HasLabelAccess() func(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (res interface{}, err error) { //nolint return func(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (res interface{}, err error) { //if totp token don't validate if UserHasMatchingRole(ctx, []model.Role{model.RoleTotpRole}) { return next(ctx) } bm, o := verifyUserAccess(ctx, obj) if bm[field] == nil { return next(ctx) } return fieldInterceptor(ctx, next, bm, o, field, func(ctx context.Context, id, o string) error { return r.checkIfUserHasLabelAccessByBanner(ctx, id, o) }) } } func (r *Resolver) HasRole() func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []model.Role) (interface{}, error) { return func(ctx context.Context, _ interface{}, next graphql.Resolver, roles []model.Role) (interface{}, error) { if UserHasMatchingRole(ctx, roles) { return next(ctx) } return nil, fmt.Errorf("access denied: user not one of %v", roles) } } // HasTerminalAccess func (r *Resolver) HasTerminalAccess() func(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (interface{}, error) { //nolint return func(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (res interface{}, err error) { if UserHasMatchingRole(ctx, []model.Role{model.RoleTotpRole}) { return next(ctx) } bm, o := verifyUserAccess(ctx, obj) if bm[field] == nil { return next(ctx) } return fieldInterceptor(ctx, next, bm, o, field, func(ctx context.Context, id, o string) error { return r.checkUserTerminalAccess(ctx, id, o) }) } } // HasValidProviderPinSetting func (r *Resolver) HasValidProviderPinSetting() func(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (interface{}, error) { //nolint return func(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (res interface{}, err error) { o := obj.(map[string]interface{}) x := o[field].(int64) if x >= 3 && x <= 5 { return next(ctx) } return nil, fmt.Errorf("%s must be between 3-5", field) } } // verifyUserAccess func verifyUserAccess(ctx context.Context, obj interface{}) (map[string]interface{}, string) { u := middleware.ForContext(ctx) if u == nil || obj == nil { return make(map[string]interface{}, 0), "" } bm := obj.(map[string]interface{}) o := bsl.GetOrgShortName(u.Organization) return bm, o } // fieldInterceptor func fieldInterceptor(ctx context.Context, next graphql.Resolver, bm map[string]interface{}, o, field string, cb func(ctx context.Context, id string, o string) error) (interface{}, error) { _type := reflect.TypeOf(bm[field]) switch _type.Kind() { case reflect.Slice: fieldParams := bm[field].([]interface{}) for i := range fieldParams { fieldParam := fieldParams[i] id, ok := fieldParam.(string) if !ok || id == "" { return next(ctx) } err := cb(ctx, id, o) if err != nil { return nil, err } } case reflect.String: id, ok := bm[field].(string) if !ok || id == "" { return next(ctx) } err := cb(ctx, id, o) if err != nil { return nil, err } default: return nil, errors.New("unknown entity type") } return next(ctx) } func getUserOrg(ctx context.Context, obj interface{}, field string) (string, bool, string) { bm, o := verifyUserAccess(ctx, obj) id, ok := bm[field].(string) return id, ok, o } func isUserAssignedToBanner(banners []*model.Banner, bannerID string) bool { for _, banner := range banners { if banner.BannerEdgeID == bannerID { return true } } return false } func (r *Resolver) checkIfUserHasLabelAccessByBanner(ctx context.Context, labelEdgeID, userOrg string) error { banners, err := r.BannerService.GetBanners(ctx) if err != nil && err.Error() != apierror.BannerCheckMessage { return err } label, err := r.LabelService.GetLabel(ctx, labelEdgeID) if err != nil { return fmt.Errorf("access denied: user org %s does not have access to requested organization", userOrg) } if label.BannerEdgeID == nil { return nil } t, errs := r.LabelService.GetLabelTenant(ctx, labelEdgeID) if errs != nil || userOrg != t.OrgName { return fmt.Errorf("access denied: user org %s does not have access to requested organization", userOrg) } if !isUserAssignedToBanner(banners, *label.BannerEdgeID) { return fmt.Errorf("access denied: user does not have access to the banner %s that the requested label resource %s exists in", *label.BannerEdgeID, labelEdgeID) } return nil } func (r *Resolver) checkUserTerminalAccess(ctx context.Context, terminalEdgeID, org string) error { getLabel := false terminal, err := r.TerminalService.GetTerminal(ctx, terminalEdgeID, &getLabel) if err != nil { return err } cluster, err := r.StoreClusterService.GetClusterByClusterEdgeID(ctx, terminal.ClusterEdgeID) if err != nil { return err } addIDsToContext(ctx, "", cluster.BannerEdgeID, terminal.ClusterEdgeID) u := middleware.ForContext(ctx) if u == nil { return fmt.Errorf("access denied: user not authenticated") } switch { case UserHasMatchingRole(ctx, []model.Role{model.RoleEdgeOrgAdmin}): return nil case UserHasMatchingRole(ctx, []model.Role{model.RoleEdgeBannerAdmin, model.RoleEdgeBannerOperator}): userAssignedBanners, err := r.BannerService.GetUserAssignedBanners(ctx, fullUsername(u.Username, org)) if err != nil { return err } for _, banner := range userAssignedBanners { if cluster.BannerEdgeID == banner.BannerEdgeID { return nil } } fallthrough default: return fmt.Errorf("access denied: user does not have access to terminal %s", terminalEdgeID) } }