package rbac import ( "crypto/sha256" "encoding/base32" "strings" "github.com/pkg/errors" "github.com/rancher/norman/types" mgmt "github.com/rancher/rancher/pkg/apis/management.cattle.io" provv1 "github.com/rancher/rancher/pkg/apis/provisioning.cattle.io/v1" v32 "github.com/rancher/rancher/pkg/generated/controllers/management.cattle.io/v3" v3 "github.com/rancher/rancher/pkg/generated/norman/management.cattle.io/v3" "github.com/rancher/rancher/pkg/ref" k8srbacv1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1" wranglerName "github.com/rancher/wrangler/v3/pkg/name" "github.com/sirupsen/logrus" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( NamespaceID = "namespaceId" ProjectID = "projectId" ClusterID = "clusterId" GlobalAdmin = "admin" GlobalRestrictedAdmin = "restricted-admin" ClusterCRDsClusterRole = "cluster-crd-clusterRole" RestrictedAdminClusterRoleBinding = "restricted-admin-rb-cluster" ProjectCRDsClusterRole = "project-crd-clusterRole" RestrictedAdminProjectRoleBinding = "restricted-admin-rb-project" RestrictedAdminCRForClusters = "restricted-admin-cr-clusters" RestrictedAdminCRBForClusters = "restricted-admin-crb-clusters" ) // BuildSubjectFromRTB This function will generate // PRTB and CRTB to the subject with user, group // or service account func BuildSubjectFromRTB(object metav1.Object) (rbacv1.Subject, error) { var userName, groupPrincipalName, groupName, name, kind, sa, namespace string switch rtb := object.(type) { case *v3.ProjectRoleTemplateBinding: userName = rtb.UserName groupPrincipalName = rtb.GroupPrincipalName groupName = rtb.GroupName sa = rtb.ServiceAccount case *v3.ClusterRoleTemplateBinding: userName = rtb.UserName groupPrincipalName = rtb.GroupPrincipalName groupName = rtb.GroupName default: objectName := "" if object != nil { objectName = object.GetName() } return rbacv1.Subject{}, errors.Errorf("unrecognized roleTemplateBinding type: %v", objectName) } if userName != "" { name = userName kind = "User" } if groupPrincipalName != "" { if name != "" { return rbacv1.Subject{}, errors.Errorf("roletemplatebinding has more than one subject fields set: %v", object.GetName()) } name = groupPrincipalName kind = "Group" } if groupName != "" { if name != "" { return rbacv1.Subject{}, errors.Errorf("roletemplatebinding has more than one subject fields set: %v", object.GetName()) } name = groupName kind = "Group" } if sa != "" { parts := strings.SplitN(sa, ":", 2) if len(parts) < 2 { return rbacv1.Subject{}, errors.Errorf("service account %s of projectroletemplatebinding is invalid: %v", sa, object.GetName()) } namespace = parts[0] name = parts[1] kind = "ServiceAccount" } if name == "" { return rbacv1.Subject{}, errors.Errorf("roletemplatebinding doesn't have any subject fields set: %v", object.GetName()) } // apiGroup default for both User and Group apiGroup := "rbac.authorization.k8s.io" if kind == "ServiceAccount" { // ServiceAccount default is empty string apiGroup = "" } return rbacv1.Subject{ Namespace: namespace, Kind: kind, Name: name, APIGroup: apiGroup, }, nil } func GrbCRBName(grb *v3.GlobalRoleBinding) string { var prefix string if grb.GlobalRoleName == GlobalAdmin { prefix = "globaladmin-" } else { prefix = "globalrestrictedadmin-" } return prefix + GetGRBTargetKey(grb) } // GetGRBSubject creates and returns a subject that is // determined by inspecting the the GRB's target fields func GetGRBSubject(grb *v3.GlobalRoleBinding) rbacv1.Subject { kind := "User" name := grb.UserName if name == "" && grb.GroupPrincipalName != "" { kind = "Group" name = grb.GroupPrincipalName } return rbacv1.Subject{ Kind: kind, Name: name, APIGroup: rbacv1.GroupName, } } // getGRBTargetKey returns a key that uniquely identifies the given GRB's target. // If a user is being targeted, then the user's name is returned. // Otherwise, the group principal name is converted to a valid user string and // is returned. func GetGRBTargetKey(grb *v3.GlobalRoleBinding) string { name := grb.UserName if name == "" { hasher := sha256.New() hasher.Write([]byte(grb.GroupPrincipalName)) sha := base32.StdEncoding.WithPadding(-1).EncodeToString(hasher.Sum(nil))[:10] name = "u-" + strings.ToLower(sha) } return name } // Returns object with available information to check against users permissions, used in combination with CanDo func ObjFromContext(apiContext *types.APIContext, resource *types.RawResource) map[string]interface{} { var obj map[string]interface{} if resource != nil && resource.Values["id"] != nil { obj = resource.Values } if obj == nil { obj = map[string]interface{}{ "id": apiContext.ID, } // collection endpoint without id needs to know which cluster-namespace for rbac check if apiContext.Query.Get(ClusterID) != "" { obj[NamespaceID] = apiContext.Query.Get(ClusterID) } if apiContext.Query.Get(ProjectID) != "" { _, obj[NamespaceID] = ref.Parse(apiContext.Query.Get(ProjectID)) } } return obj } func TypeFromContext(apiContext *types.APIContext, resource *types.RawResource) string { if resource == nil { return apiContext.Type } return resource.Type } func GetRTBLabel(objMeta metav1.ObjectMeta) string { return wranglerName.SafeConcatName(objMeta.Namespace + "_" + objMeta.Name) } // NameForRoleBinding returns a deterministic name for a RoleBinding with the provided namespace, roleName, and subject func NameForRoleBinding(namespace string, role rbacv1.RoleRef, subject rbacv1.Subject) string { var name strings.Builder name.WriteString("rb-") name.WriteString(getBindingHash(namespace, role, subject)) nm := name.String() logrus.Debugf("RoleBinding with namespace=%s role.kind=%s role.name=%s subject.kind=%s subject.name=%s has name: %s", namespace, role.Kind, role.Name, subject.Kind, subject.Name, nm) return nm } // NameForClusterRoleBinding returns a deterministic name for a ClusterRoleBinding with the provided roleName and subject func NameForClusterRoleBinding(role rbacv1.RoleRef, subject rbacv1.Subject) string { var name strings.Builder name.WriteString("crb-") name.WriteString(getBindingHash("", role, subject)) nm := name.String() logrus.Debugf("ClusterRoleBinding with role.kind=%s role.name=%s subject.kind=%s subject.name=%s has name: %s", role.Kind, role.Name, subject.Kind, subject.Name, nm) return nm } // getBindingHash returns a hash created from the passed in arguments // uses base32 encoding for hash, since all characters in encoding scheme are valid in k8s resource names // probability of collision is: 1/32^10 == 1/(2^5)^10 == 1/2^50 (sufficiently low) func getBindingHash(namespace string, role rbacv1.RoleRef, subject rbacv1.Subject) string { var input strings.Builder input.WriteString(namespace) input.WriteString(role.Kind) input.WriteString(role.Name) input.WriteString(subject.Kind) input.WriteString(subject.Name) hasher := sha256.New() hasher.Write([]byte(input.String())) digest := base32.StdEncoding.WithPadding(-1).EncodeToString(hasher.Sum(nil)) return strings.ToLower(digest[:10]) } // RulesFromTemplate gets all rules from the template and all referenced templates func RulesFromTemplate(clusterRoles k8srbacv1.ClusterRoleCache, roleTemplates v32.RoleTemplateCache, rt *v3.RoleTemplate) ([]rbacv1.PolicyRule, error) { var rules []rbacv1.PolicyRule var err error templatesSeen := make(map[string]bool) // Kickoff gathering rules rules, err = gatherRules(clusterRoles, roleTemplates, rt, rules, templatesSeen) if err != nil { return rules, err } return rules, nil } // gatherRules appends the rules from current template and does a recursive call to get all inherited roles referenced func gatherRules(clusterRoles k8srbacv1.ClusterRoleCache, roleTemplates v32.RoleTemplateCache, rt *v3.RoleTemplate, rules []rbacv1.PolicyRule, seen map[string]bool) ([]rbacv1.PolicyRule, error) { seen[rt.Name] = true if rt.External { if rt.ExternalRules != nil { rules = append(rules, rt.ExternalRules...) } else if rt.Context == "cluster" { cr, err := clusterRoles.Get(rt.Name) if err != nil { return nil, err } rules = append(rules, cr.Rules...) } } rules = append(rules, rt.Rules...) for _, r := range rt.RoleTemplateNames { // If we have already seen the roleTemplate, skip it if seen[r] { continue } next, err := roleTemplates.Get(r) if err != nil { return nil, err } rules, err = gatherRules(clusterRoles, roleTemplates, next, rules, seen) if err != nil { return nil, err } } return rules, nil } func ProvisioningClusterAdminName(cluster *provv1.Cluster) string { return wranglerName.SafeConcatName("crt", cluster.Name, "cluster-owner") } func RuleGivesResourceAccess(rule rbacv1.PolicyRule, resourceName string) bool { if !isRuleInTargetAPIGroup(rule) { // if we don't list the target api group, don't bother looking for the resources return false } for _, resource := range rule.Resources { if resource == resourceName || resource == "*" { return true } } return false } func isRuleInTargetAPIGroup(rule rbacv1.PolicyRule) bool { for _, group := range rule.APIGroups { if group == mgmt.GroupName || group == "*" { return true } } return false }