/* Copyright 2019 The KubeSphere Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package iam import ( "fmt" "github.com/go-ldap/ldap" "github.com/golang/glog" rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "kubesphere.io/kubesphere/pkg/constants" "kubesphere.io/kubesphere/pkg/informers" "kubesphere.io/kubesphere/pkg/models" "kubesphere.io/kubesphere/pkg/models/iam/policy" "kubesphere.io/kubesphere/pkg/models/kubectl" "kubesphere.io/kubesphere/pkg/models/resources" "kubesphere.io/kubesphere/pkg/params" "kubesphere.io/kubesphere/pkg/simple/client/k8s" "kubesphere.io/kubesphere/pkg/utils/k8sutil" "kubesphere.io/kubesphere/pkg/utils/sliceutil" "sort" "strings" "time" ) const ( ClusterRoleKind = "ClusterRole" NamespaceAdminRoleBindName = "admin" NamespaceViewerRoleBindName = "viewer" ) func GetDevopsRoleSimpleRules(role string) []models.SimpleRule { var rules []models.SimpleRule switch role { case "developer": rules = []models.SimpleRule{ {Name: "pipelines", Actions: []string{"view", "trigger"}}, {Name: "roles", Actions: []string{"view"}}, {Name: "members", Actions: []string{"view"}}, {Name: "devops", Actions: []string{"view"}}, } break case "owner": rules = []models.SimpleRule{ {Name: "pipelines", Actions: []string{"create", "edit", "view", "delete", "trigger"}}, {Name: "roles", Actions: []string{"view"}}, {Name: "members", Actions: []string{"create", "edit", "view", "delete"}}, {Name: "credentials", Actions: []string{"create", "edit", "view", "delete"}}, {Name: "devops", Actions: []string{"edit", "view", "delete"}}, } break case "maintainer": rules = []models.SimpleRule{ {Name: "pipelines", Actions: []string{"create", "edit", "view", "delete", "trigger"}}, {Name: "roles", Actions: []string{"view"}}, {Name: "members", Actions: []string{"view"}}, {Name: "credentials", Actions: []string{"create", "edit", "view", "delete"}}, {Name: "devops", Actions: []string{"view"}}, } break case "reporter": fallthrough default: rules = []models.SimpleRule{ {Name: "pipelines", Actions: []string{"view"}}, {Name: "roles", Actions: []string{"view"}}, {Name: "members", Actions: []string{"view"}}, {Name: "devops", Actions: []string{"view"}}, } break } return rules } // Get user roles in namespace func GetUserRoles(namespace, username string) ([]*rbacv1.Role, error) { clusterRoleLister := informers.SharedInformerFactory().Rbac().V1().ClusterRoles().Lister() roleBindingLister := informers.SharedInformerFactory().Rbac().V1().RoleBindings().Lister() roleLister := informers.SharedInformerFactory().Rbac().V1().Roles().Lister() roleBindings, err := roleBindingLister.RoleBindings(namespace).List(labels.Everything()) if err != nil { glog.Errorln("get role bindings", namespace, err) return nil, err } roles := make([]*rbacv1.Role, 0) for _, roleBinding := range roleBindings { if k8sutil.ContainsUser(roleBinding.Subjects, username) { if roleBinding.RoleRef.Kind == ClusterRoleKind { clusterRole, err := clusterRoleLister.Get(roleBinding.RoleRef.Name) if err != nil { if apierrors.IsNotFound(err) { glog.Warningf("cluster role %s not found but bind user %s in namespace %s", roleBinding.RoleRef.Name, username, namespace) continue } else { glog.Errorln("get cluster role", roleBinding.RoleRef.Name, err) return nil, err } } role := rbacv1.Role{} role.TypeMeta = clusterRole.TypeMeta role.ObjectMeta = clusterRole.ObjectMeta role.Rules = clusterRole.Rules role.Namespace = roleBinding.Namespace roles = append(roles, &role) } else { role, err := roleLister.Roles(roleBinding.Namespace).Get(roleBinding.RoleRef.Name) if err != nil { if apierrors.IsNotFound(err) { glog.Warningf("namespace %s role %s not found, but bind user %s", namespace, roleBinding.RoleRef.Name, username) continue } else { glog.Errorln("get role", roleBinding.Namespace, roleBinding.RoleRef.Name, err) return nil, err } } roles = append(roles, role) } } } return roles, nil } func GetUserClusterRoles(username string) (*rbacv1.ClusterRole, []*rbacv1.ClusterRole, error) { clusterRoleLister := informers.SharedInformerFactory().Rbac().V1().ClusterRoles().Lister() clusterRoleBindingLister := informers.SharedInformerFactory().Rbac().V1().ClusterRoleBindings().Lister() clusterRoleBindings, err := clusterRoleBindingLister.List(labels.Everything()) if err != nil { glog.Errorln("get cluster role bindings", err) return nil, nil, err } clusterRoles := make([]*rbacv1.ClusterRole, 0) userFacingClusterRole := &rbacv1.ClusterRole{} for _, clusterRoleBinding := range clusterRoleBindings { if k8sutil.ContainsUser(clusterRoleBinding.Subjects, username) { clusterRole, err := clusterRoleLister.Get(clusterRoleBinding.RoleRef.Name) if err != nil { if apierrors.IsNotFound(err) { glog.Warningf("cluster role %s not found but bind user %s", clusterRoleBinding.RoleRef.Name, username) continue } else { glog.Errorln("get cluster role", clusterRoleBinding.RoleRef.Name, err) return nil, nil, err } } if clusterRoleBinding.Name == username { userFacingClusterRole = clusterRole } clusterRoles = append(clusterRoles, clusterRole) } } return userFacingClusterRole, clusterRoles, nil } func GetUserClusterRole(username string) (*rbacv1.ClusterRole, error) { userFacingClusterRole, _, err := GetUserClusterRoles(username) if err != nil { return nil, err } return userFacingClusterRole, nil } func GetUserClusterRules(username string) ([]rbacv1.PolicyRule, error) { _, clusterRoles, err := GetUserClusterRoles(username) if err != nil { return nil, err } rules := make([]rbacv1.PolicyRule, 0) for _, clusterRole := range clusterRoles { rules = append(rules, clusterRole.Rules...) } return rules, nil } func GetUserRules(namespace, username string) ([]rbacv1.PolicyRule, error) { roles, err := GetUserRoles(namespace, username) if err != nil { return nil, err } rules := make([]rbacv1.PolicyRule, 0) for _, role := range roles { rules = append(rules, role.Rules...) } return rules, nil } func GetWorkspaceRoleBindings(workspace string) ([]*rbacv1.ClusterRoleBinding, error) { clusterRoleBindings, err := informers.SharedInformerFactory().Rbac().V1().ClusterRoleBindings().Lister().List(labels.Everything()) if err != nil { glog.Errorln("get cluster role bindings", err) return nil, err } result := make([]*rbacv1.ClusterRoleBinding, 0) for _, roleBinding := range clusterRoleBindings { if k8sutil.IsControlledBy(roleBinding.OwnerReferences, "Workspace", workspace) { result = append(result, roleBinding) } } return result, nil } func GetWorkspaceRole(workspace, role string) (*rbacv1.ClusterRole, error) { if !sliceutil.HasString(constants.WorkSpaceRoles, role) { return nil, apierrors.NewNotFound(schema.GroupResource{Resource: "workspace role"}, role) } role = fmt.Sprintf("workspace:%s:%s", workspace, strings.TrimPrefix(role, "workspace-")) return informers.SharedInformerFactory().Rbac().V1().ClusterRoles().Lister().Get(role) } func GetUserWorkspaceRoleMap(username string) (map[string]string, error) { clusterRoleBindings, err := informers.SharedInformerFactory().Rbac().V1().ClusterRoleBindings().Lister().List(labels.Everything()) if err != nil { glog.Errorln("get cluster role bindings", err) return nil, err } result := make(map[string]string, 0) for _, roleBinding := range clusterRoleBindings { if workspace := k8sutil.GetControlledWorkspace(roleBinding.OwnerReferences); workspace != "" && k8sutil.ContainsUser(roleBinding.Subjects, username) { result[workspace] = roleBinding.RoleRef.Name } } return result, nil } func GetUserWorkspaceRole(workspace, username string) (*rbacv1.ClusterRole, error) { workspaceRoleMap, err := GetUserWorkspaceRoleMap(username) if err != nil { return nil, err } if workspaceRole := workspaceRoleMap[workspace]; workspaceRole != "" { return informers.SharedInformerFactory().Rbac().V1().ClusterRoles().Lister().Get(workspaceRole) } return nil, apierrors.NewNotFound(schema.GroupResource{Resource: "workspace user"}, username) } func GetRoleBindings(namespace string, roleName string) ([]*rbacv1.RoleBinding, error) { roleBindingLister := informers.SharedInformerFactory().Rbac().V1().RoleBindings().Lister() roleBindings, err := roleBindingLister.RoleBindings(namespace).List(labels.Everything()) if err != nil { glog.Errorln("get role bindings", namespace, err) return nil, err } items := make([]*rbacv1.RoleBinding, 0) for _, roleBinding := range roleBindings { if roleName == "" { items = append(items, roleBinding) } else if roleBinding.RoleRef.Name == roleName { items = append(items, roleBinding) } } return items, nil } func GetClusterRoleBindings(clusterRoleName string) ([]*rbacv1.ClusterRoleBinding, error) { clusterRoleBindingLister := informers.SharedInformerFactory().Rbac().V1().ClusterRoleBindings().Lister() roleBindings, err := clusterRoleBindingLister.List(labels.Everything()) if err != nil { glog.Errorln("get cluster role bindings", err) return nil, err } items := make([]*rbacv1.ClusterRoleBinding, 0) for _, roleBinding := range roleBindings { if roleBinding.RoleRef.Name == clusterRoleName { items = append(items, roleBinding) } } return items, nil } func ListClusterRoleUsers(clusterRoleName string, conditions *params.Conditions, orderBy string, reverse bool, limit, offset int) (*models.PageableResponse, error) { roleBindings, err := GetClusterRoleBindings(clusterRoleName) if err != nil { return nil, err } users := make([]*models.User, 0) for _, roleBinding := range roleBindings { for _, subject := range roleBinding.Subjects { if subject.Kind == rbacv1.UserKind && !k8sutil.ContainsUser(users, subject.Name) { user, err := GetUserInfo(subject.Name) if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) { continue } if err != nil { glog.Errorln("get user info", subject.Name, err) return nil, err } users = append(users, user) } } } // order & reverse sort.Slice(users, func(i, j int) bool { if reverse { tmp := i i = j j = tmp } switch orderBy { default: fallthrough case resources.Name: return strings.Compare(users[i].Username, users[j].Username) <= 0 } }) result := make([]interface{}, 0) for i, d := range users { if i >= offset && (limit == -1 || len(result) < limit) { result = append(result, d) } } return &models.PageableResponse{Items: result, TotalCount: len(users)}, nil } func RoleUsers(namespace string, roleName string) ([]*models.User, error) { roleBindings, err := GetRoleBindings(namespace, roleName) if err != nil { return nil, err } users := make([]*models.User, 0) for _, roleBinding := range roleBindings { for _, subject := range roleBinding.Subjects { if subject.Kind == rbacv1.UserKind && !k8sutil.ContainsUser(users, subject.Name) { user, err := GetUserInfo(subject.Name) if err != nil { if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) { continue } return nil, err } user.Role = roleBinding.RoleRef.Name users = append(users, user) } } } return users, nil } func ListRoles(namespace string, conditions *params.Conditions, orderBy string, reverse bool, limit, offset int) (*models.PageableResponse, error) { return resources.ListResources(namespace, resources.Roles, conditions, orderBy, reverse, limit, offset) } func ListWorkspaceRoles(workspace string, conditions *params.Conditions, orderBy string, reverse bool, limit, offset int) (*models.PageableResponse, error) { conditions.Match[resources.OwnerName] = workspace conditions.Match[resources.OwnerKind] = "Workspace" result, err := resources.ListResources("", resources.ClusterRoles, conditions, orderBy, reverse, limit, offset) if err != nil { return nil, err } for i, item := range result.Items { if role, ok := item.(*rbacv1.ClusterRole); ok { role = role.DeepCopy() role.Name = role.Annotations[constants.DisplayNameAnnotationKey] result.Items[i] = role } } return result, nil } func ListClusterRoles(conditions *params.Conditions, orderBy string, reverse bool, limit, offset int) (*models.PageableResponse, error) { return resources.ListResources("", resources.ClusterRoles, conditions, orderBy, reverse, limit, offset) } func NamespaceUsers(namespaceName string) ([]*models.User, error) { namespace, err := informers.SharedInformerFactory().Core().V1().Namespaces().Lister().Get(namespaceName) if err != nil { glog.Errorln("get namespace", namespaceName, err) return nil, err } roleBindings, err := GetRoleBindings(namespaceName, "") if err != nil { return nil, err } users := make([]*models.User, 0) for _, roleBinding := range roleBindings { // controlled by ks-controller-manager if roleBinding.Name == NamespaceViewerRoleBindName { continue } for _, subject := range roleBinding.Subjects { if subject.Kind == rbacv1.UserKind && !k8sutil.ContainsUser(users, subject.Name) { // show creator if roleBinding.Name == NamespaceAdminRoleBindName && subject.Name != namespace.Annotations[constants.CreatorAnnotationKey] { continue } user, err := GetUserInfo(subject.Name) if err != nil { if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) { continue } return nil, err } user.Role = roleBinding.RoleRef.Name user.RoleBindTime = &roleBinding.CreationTimestamp.Time user.RoleBinding = roleBinding.Name users = append(users, user) } } } return users, nil } func GetUserWorkspaceSimpleRules(workspace, username string) ([]models.SimpleRule, error) { clusterRules, err := GetUserClusterRules(username) if err != nil { return nil, err } // workspace manager if RulesMatchesRequired(clusterRules, rbacv1.PolicyRule{ Verbs: []string{"*"}, APIGroups: []string{"*"}, Resources: []string{"workspaces", "workspaces/*"}, }) { return GetWorkspaceRoleSimpleRules(workspace, constants.WorkspaceAdmin), nil } workspaceRole, err := GetUserWorkspaceRole(workspace, username) if err != nil { if apierrors.IsNotFound(err) { return []models.SimpleRule{}, nil } return nil, err } return GetWorkspaceRoleSimpleRules(workspace, workspaceRole.Annotations[constants.DisplayNameAnnotationKey]), nil } func GetWorkspaceRoleSimpleRules(workspace, roleName string) []models.SimpleRule { workspaceRules := make([]models.SimpleRule, 0) switch roleName { case constants.WorkspaceAdmin: workspaceRules = []models.SimpleRule{ {Name: "workspaces", Actions: []string{"edit", "delete", "view"}}, {Name: "members", Actions: []string{"edit", "delete", "create", "view"}}, {Name: "devops", Actions: []string{"edit", "delete", "create", "view"}}, {Name: "projects", Actions: []string{"edit", "delete", "create", "view"}}, {Name: "roles", Actions: []string{"view"}}, } case constants.WorkspaceRegular: workspaceRules = []models.SimpleRule{ {Name: "members", Actions: []string{"view"}}, {Name: "devops", Actions: []string{"create"}}, {Name: "projects", Actions: []string{"create"}}, } case constants.WorkspaceViewer: workspaceRules = []models.SimpleRule{ {Name: "workspaces", Actions: []string{"view"}}, {Name: "members", Actions: []string{"view"}}, {Name: "devops", Actions: []string{"view"}}, {Name: "projects", Actions: []string{"view"}}, {Name: "roles", Actions: []string{"view"}}, } } return workspaceRules } // Convert cluster role to rules func GetClusterRoleSimpleRules(clusterRoleName string) ([]models.SimpleRule, error) { clusterRoleLister := informers.SharedInformerFactory().Rbac().V1().ClusterRoles().Lister() clusterRole, err := clusterRoleLister.Get(clusterRoleName) if err != nil { glog.Errorln("get cluster role", clusterRoleName, clusterRoleName) return nil, err } return getClusterSimpleRule(clusterRole.Rules), nil } func GetUserClusterSimpleRules(username string) ([]models.SimpleRule, error) { clusterRules, err := GetUserClusterRules(username) if err != nil { return nil, err } return getClusterSimpleRule(clusterRules), nil } func GetUserNamespaceSimpleRules(namespace, username string) ([]models.SimpleRule, error) { clusterRules, err := GetUserClusterRules(username) if err != nil { return nil, err } rules, err := GetUserRules(namespace, username) if err != nil { return nil, err } rules = append(rules, clusterRules...) return getSimpleRule(rules), nil } // Convert roles to rules func GetRoleSimpleRules(namespace string, roleName string) ([]models.SimpleRule, error) { roleLister := informers.SharedInformerFactory().Rbac().V1().Roles().Lister() role, err := roleLister.Roles(namespace).Get(roleName) if err != nil { glog.Errorln("get role", namespace, roleName, err) return nil, err } return getSimpleRule(role.Rules), nil } func getClusterSimpleRule(policyRules []rbacv1.PolicyRule) []models.SimpleRule { rules := make([]models.SimpleRule, 0) for i := 0; i < len(policy.ClusterRoleRuleMapping); i++ { validActions := make([]string, 0) for j := 0; j < (len(policy.ClusterRoleRuleMapping[i].Actions)); j++ { if rulesMatchesAction(policyRules, policy.ClusterRoleRuleMapping[i].Actions[j]) { validActions = append(validActions, policy.ClusterRoleRuleMapping[i].Actions[j].Name) } } if len(validActions) > 0 { rules = append(rules, models.SimpleRule{Name: policy.ClusterRoleRuleMapping[i].Name, Actions: validActions}) } } return rules } func getSimpleRule(policyRules []rbacv1.PolicyRule) []models.SimpleRule { simpleRules := make([]models.SimpleRule, 0) for i := 0; i < len(policy.RoleRuleMapping); i++ { rule := models.SimpleRule{Name: policy.RoleRuleMapping[i].Name} rule.Actions = make([]string, 0) for j := 0; j < len(policy.RoleRuleMapping[i].Actions); j++ { if rulesMatchesAction(policyRules, policy.RoleRuleMapping[i].Actions[j]) { rule.Actions = append(rule.Actions, policy.RoleRuleMapping[i].Actions[j].Name) } } if len(rule.Actions) > 0 { simpleRules = append(simpleRules, rule) } } return simpleRules } func CreateClusterRoleBinding(username string, clusterRoleName string) error { clusterRoleLister := informers.SharedInformerFactory().Rbac().V1().ClusterRoles().Lister() _, err := clusterRoleLister.Get(clusterRoleName) if err != nil { glog.Errorln("get cluster role", clusterRoleName, err) return err } if clusterRoleName == constants.ClusterAdmin { // create kubectl pod if cluster role is cluster-admin if err := kubectl.CreateKubectlDeploy(username); err != nil { glog.Error("create user terminal pod failed", username, err) } } else { // delete kubectl pod if cluster role is not cluster-admin, whether it exists or not if err := kubectl.DelKubectlDeploy(username); err != nil { glog.Error("delete user terminal pod failed", username, err) } } clusterRoleBinding := &rbacv1.ClusterRoleBinding{} clusterRoleBinding.Name = username clusterRoleBinding.RoleRef = rbacv1.RoleRef{Name: clusterRoleName, Kind: ClusterRoleKind} clusterRoleBinding.Subjects = []rbacv1.Subject{{Kind: rbacv1.UserKind, Name: username}} clusterRoleBindingLister := informers.SharedInformerFactory().Rbac().V1().ClusterRoleBindings().Lister() found, err := clusterRoleBindingLister.Get(username) if apierrors.IsNotFound(err) { _, err = k8s.Client().RbacV1().ClusterRoleBindings().Create(clusterRoleBinding) if err != nil { glog.Errorln("create cluster role binding", err) return err } return nil } else if err != nil { return err } // cluster role changed if found.RoleRef.Name != clusterRoleName { deletePolicy := metav1.DeletePropagationForeground gracePeriodSeconds := int64(0) deleteOption := &metav1.DeleteOptions{PropagationPolicy: &deletePolicy, GracePeriodSeconds: &gracePeriodSeconds} err = k8s.Client().RbacV1().ClusterRoleBindings().Delete(found.Name, deleteOption) if err != nil { glog.Errorln("delete cluster role binding", err) return err } maxRetries := 3 for i := 0; i < maxRetries; i++ { _, err = k8s.Client().RbacV1().ClusterRoleBindings().Create(clusterRoleBinding) if err == nil { return nil } time.Sleep(300 * time.Millisecond) } glog.Errorln("create cluster role binding", err) return err } if !k8sutil.ContainsUser(found.Subjects, username) { found.Subjects = clusterRoleBinding.Subjects _, err = k8s.Client().RbacV1().ClusterRoleBindings().Update(found) if err != nil { glog.Errorln("update cluster role binding", err) return err } } return nil } func RulesMatchesRequired(rules []rbacv1.PolicyRule, required rbacv1.PolicyRule) bool { for _, rule := range rules { if ruleMatchesRequired(rule, required) { return true } } return false } func rulesMatchesAction(rules []rbacv1.PolicyRule, action models.Action) bool { for _, required := range action.Rules { if !RulesMatchesRequired(rules, required) { return false } } return true } func ruleMatchesRequired(rule rbacv1.PolicyRule, required rbacv1.PolicyRule) bool { if len(required.NonResourceURLs) == 0 { for _, apiGroup := range required.APIGroups { for _, resource := range required.Resources { resources := strings.Split(resource, "/") resource = resources[0] var subsource string if len(resources) > 1 { subsource = resources[1] } if len(required.ResourceNames) == 0 { for _, verb := range required.Verbs { if !ruleMatchesRequest(rule, apiGroup, "", resource, subsource, "", verb) { return false } } } else { for _, resourceName := range required.ResourceNames { for _, verb := range required.Verbs { if !ruleMatchesRequest(rule, apiGroup, "", resource, subsource, resourceName, verb) { return false } } } } } } } else { for _, apiGroup := range required.APIGroups { for _, nonResourceURL := range required.NonResourceURLs { for _, verb := range required.Verbs { if !ruleMatchesRequest(rule, apiGroup, nonResourceURL, "", "", "", verb) { return false } } } } } return true } func ruleMatchesResources(rule rbacv1.PolicyRule, apiGroup string, resource string, subresource string, resourceName string) bool { if resource == "" { return false } if !hasString(rule.APIGroups, apiGroup) && !hasString(rule.APIGroups, rbacv1.ResourceAll) { return false } if len(rule.ResourceNames) > 0 && !hasString(rule.ResourceNames, resourceName) { return false } combinedResource := resource if subresource != "" { combinedResource = combinedResource + "/" + subresource } for _, res := range rule.Resources { // match "*" if res == rbacv1.ResourceAll || res == combinedResource { return true } // match "*/subresource" if len(subresource) > 0 && strings.HasPrefix(res, "*/") && subresource == strings.TrimLeft(res, "*/") { return true } // match "resource/*" if strings.HasSuffix(res, "/*") && resource == strings.TrimRight(res, "/*") { return true } } return false } func ruleMatchesRequest(rule rbacv1.PolicyRule, apiGroup string, nonResourceURL string, resource string, subresource string, resourceName string, verb string) bool { if !hasString(rule.Verbs, verb) && !hasString(rule.Verbs, rbacv1.VerbAll) { return false } if nonResourceURL == "" { return ruleMatchesResources(rule, apiGroup, resource, subresource, resourceName) } else { return ruleMatchesNonResource(rule, nonResourceURL) } } func ruleMatchesNonResource(rule rbacv1.PolicyRule, nonResourceURL string) bool { if nonResourceURL == "" { return false } for _, spec := range rule.NonResourceURLs { if pathMatches(nonResourceURL, spec) { return true } } return false } func pathMatches(path, spec string) bool { // Allow wildcard match if spec == "*" { return true } // Allow exact match if spec == path { return true } // Allow a trailing * subpath match if strings.HasSuffix(spec, "*") && strings.HasPrefix(path, strings.TrimRight(spec, "*")) { return true } return false } func hasString(slice []string, value string) bool { for _, s := range slice { if s == value { return true } } return false }