github.com/TheSpiritXIII/controller-tools@v0.14.1/pkg/rbac/parser.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package rbac contain libraries for generating RBAC manifests from RBAC
    18  // markers in Go source files.
    19  //
    20  // The markers take the form:
    21  //
    22  //	+kubebuilder:rbac:groups=<groups>,resources=<resources>,resourceNames=<resource names>,verbs=<verbs>,urls=<non resource urls>
    23  package rbac
    24  
    25  import (
    26  	"fmt"
    27  	"sort"
    28  	"strings"
    29  
    30  	rbacv1 "k8s.io/api/rbac/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  
    33  	"github.com/TheSpiritXIII/controller-tools/pkg/genall"
    34  	"github.com/TheSpiritXIII/controller-tools/pkg/markers"
    35  )
    36  
    37  var (
    38  	// RuleDefinition is a marker for defining RBAC rules.
    39  	// Call ToRule on the value to get a Kubernetes RBAC policy rule.
    40  	RuleDefinition = markers.Must(markers.MakeDefinition("kubebuilder:rbac", markers.DescribesPackage, Rule{}))
    41  )
    42  
    43  // +controllertools:marker:generateHelp:category=RBAC
    44  
    45  // Rule specifies an RBAC rule to all access to some resources or non-resource URLs.
    46  type Rule struct {
    47  	// Groups specifies the API groups that this rule encompasses.
    48  	Groups []string `marker:",optional"`
    49  	// Resources specifies the API resources that this rule encompasses.
    50  	Resources []string `marker:",optional"`
    51  	// ResourceNames specifies the names of the API resources that this rule encompasses.
    52  	//
    53  	// Create requests cannot be restricted by resourcename, as the object's name
    54  	// is not known at authorization time.
    55  	ResourceNames []string `marker:",optional"`
    56  	// Verbs specifies the (lowercase) kubernetes API verbs that this rule encompasses.
    57  	Verbs []string
    58  	// URL specifies the non-resource URLs that this rule encompasses.
    59  	URLs []string `marker:"urls,optional"`
    60  	// Namespace specifies the scope of the Rule.
    61  	// If not set, the Rule belongs to the generated ClusterRole.
    62  	// If set, the Rule belongs to a Role, whose namespace is specified by this field.
    63  	Namespace string `marker:",optional"`
    64  }
    65  
    66  // ruleKey represents the resources and non-resources a Rule applies.
    67  type ruleKey struct {
    68  	Groups        string
    69  	Resources     string
    70  	ResourceNames string
    71  	URLs          string
    72  }
    73  
    74  func (key ruleKey) String() string {
    75  	return fmt.Sprintf("%s + %s + %s + %s", key.Groups, key.Resources, key.ResourceNames, key.URLs)
    76  }
    77  
    78  // ruleKeys implements sort.Interface
    79  type ruleKeys []ruleKey
    80  
    81  func (keys ruleKeys) Len() int           { return len(keys) }
    82  func (keys ruleKeys) Swap(i, j int)      { keys[i], keys[j] = keys[j], keys[i] }
    83  func (keys ruleKeys) Less(i, j int) bool { return keys[i].String() < keys[j].String() }
    84  
    85  // key normalizes the Rule and returns a ruleKey object.
    86  func (r *Rule) key() ruleKey {
    87  	r.normalize()
    88  	return ruleKey{
    89  		Groups:        strings.Join(r.Groups, "&"),
    90  		Resources:     strings.Join(r.Resources, "&"),
    91  		ResourceNames: strings.Join(r.ResourceNames, "&"),
    92  		URLs:          strings.Join(r.URLs, "&"),
    93  	}
    94  }
    95  
    96  // addVerbs adds new verbs into a Rule.
    97  // The duplicates in `r.Verbs` will be removed, and then `r.Verbs` will be sorted.
    98  func (r *Rule) addVerbs(verbs []string) {
    99  	r.Verbs = removeDupAndSort(append(r.Verbs, verbs...))
   100  }
   101  
   102  // normalize removes duplicates from each field of a Rule, and sorts each field.
   103  func (r *Rule) normalize() {
   104  	r.Groups = removeDupAndSort(r.Groups)
   105  	r.Resources = removeDupAndSort(r.Resources)
   106  	r.ResourceNames = removeDupAndSort(r.ResourceNames)
   107  	r.Verbs = removeDupAndSort(r.Verbs)
   108  	r.URLs = removeDupAndSort(r.URLs)
   109  }
   110  
   111  // removeDupAndSort removes duplicates in strs, sorts the items, and returns a
   112  // new slice of strings.
   113  func removeDupAndSort(strs []string) []string {
   114  	set := make(map[string]bool)
   115  	for _, str := range strs {
   116  		if _, ok := set[str]; !ok {
   117  			set[str] = true
   118  		}
   119  	}
   120  
   121  	var result []string
   122  	for str := range set {
   123  		result = append(result, str)
   124  	}
   125  	sort.Strings(result)
   126  	return result
   127  }
   128  
   129  // ToRule converts this rule to its Kubernetes API form.
   130  func (r *Rule) ToRule() rbacv1.PolicyRule {
   131  	// fix the group names first, since letting people type "core" is nice
   132  	for i, group := range r.Groups {
   133  		if group == "core" {
   134  			r.Groups[i] = ""
   135  		}
   136  	}
   137  	return rbacv1.PolicyRule{
   138  		APIGroups:       r.Groups,
   139  		Verbs:           r.Verbs,
   140  		Resources:       r.Resources,
   141  		ResourceNames:   r.ResourceNames,
   142  		NonResourceURLs: r.URLs,
   143  	}
   144  }
   145  
   146  // +controllertools:marker:generateHelp
   147  
   148  // Generator generates ClusterRole objects.
   149  type Generator struct {
   150  	// RoleName sets the name of the generated ClusterRole.
   151  	RoleName string
   152  
   153  	// HeaderFile specifies the header text (e.g. license) to prepend to generated files.
   154  	HeaderFile string `marker:",optional"`
   155  
   156  	// Year specifies the year to substitute for " YEAR" in the header file.
   157  	Year string `marker:",optional"`
   158  }
   159  
   160  func (Generator) RegisterMarkers(into *markers.Registry) error {
   161  	if err := into.Register(RuleDefinition); err != nil {
   162  		return err
   163  	}
   164  	into.AddHelp(RuleDefinition, Rule{}.Help())
   165  	return nil
   166  }
   167  
   168  // GenerateRoles generate a slice of objs representing either a ClusterRole or a Role object
   169  // The order of the objs in the returned slice is stable and determined by their namespaces.
   170  func GenerateRoles(ctx *genall.GenerationContext, roleName string) ([]interface{}, error) {
   171  	rulesByNS := make(map[string][]*Rule)
   172  	for _, root := range ctx.Roots {
   173  		markerSet, err := markers.PackageMarkers(ctx.Collector, root)
   174  		if err != nil {
   175  			root.AddError(err)
   176  		}
   177  
   178  		// group RBAC markers by namespace
   179  		for _, markerValue := range markerSet[RuleDefinition.Name] {
   180  			rule := markerValue.(Rule)
   181  			namespace := rule.Namespace
   182  			if _, ok := rulesByNS[namespace]; !ok {
   183  				rules := make([]*Rule, 0)
   184  				rulesByNS[namespace] = rules
   185  			}
   186  			rulesByNS[namespace] = append(rulesByNS[namespace], &rule)
   187  		}
   188  	}
   189  
   190  	// NormalizeRules merge Rule with the same ruleKey and sort the Rules
   191  	NormalizeRules := func(rules []*Rule) []rbacv1.PolicyRule {
   192  		ruleMap := make(map[ruleKey]*Rule)
   193  		// all the Rules having the same ruleKey will be merged into the first Rule
   194  		for _, rule := range rules {
   195  			key := rule.key()
   196  			if _, ok := ruleMap[key]; !ok {
   197  				ruleMap[key] = rule
   198  				continue
   199  			}
   200  			ruleMap[key].addVerbs(rule.Verbs)
   201  		}
   202  
   203  		// sort the Rules in rules according to their ruleKeys
   204  		keys := make([]ruleKey, 0, len(ruleMap))
   205  		for key := range ruleMap {
   206  			keys = append(keys, key)
   207  		}
   208  		sort.Sort(ruleKeys(keys))
   209  
   210  		var policyRules []rbacv1.PolicyRule
   211  		for _, key := range keys {
   212  			policyRules = append(policyRules, ruleMap[key].ToRule())
   213  
   214  		}
   215  		return policyRules
   216  	}
   217  
   218  	// collect all the namespaces and sort them
   219  	var namespaces []string
   220  	for ns := range rulesByNS {
   221  		namespaces = append(namespaces, ns)
   222  	}
   223  	sort.Strings(namespaces)
   224  
   225  	// process the items in rulesByNS by the order specified in `namespaces` to make sure that the Role order is stable
   226  	var objs []interface{}
   227  	for _, ns := range namespaces {
   228  		rules := rulesByNS[ns]
   229  		policyRules := NormalizeRules(rules)
   230  		if len(policyRules) == 0 {
   231  			continue
   232  		}
   233  		if ns == "" {
   234  			objs = append(objs, rbacv1.ClusterRole{
   235  				TypeMeta: metav1.TypeMeta{
   236  					Kind:       "ClusterRole",
   237  					APIVersion: rbacv1.SchemeGroupVersion.String(),
   238  				},
   239  				ObjectMeta: metav1.ObjectMeta{
   240  					Name: roleName,
   241  				},
   242  				Rules: policyRules,
   243  			})
   244  		} else {
   245  			objs = append(objs, rbacv1.Role{
   246  				TypeMeta: metav1.TypeMeta{
   247  					Kind:       "Role",
   248  					APIVersion: rbacv1.SchemeGroupVersion.String(),
   249  				},
   250  				ObjectMeta: metav1.ObjectMeta{
   251  					Name:      roleName,
   252  					Namespace: ns,
   253  				},
   254  				Rules: policyRules,
   255  			})
   256  		}
   257  	}
   258  
   259  	return objs, nil
   260  }
   261  
   262  func (g Generator) Generate(ctx *genall.GenerationContext) error {
   263  	objs, err := GenerateRoles(ctx, g.RoleName)
   264  	if err != nil {
   265  		return err
   266  	}
   267  
   268  	if len(objs) == 0 {
   269  		return nil
   270  	}
   271  
   272  	var headerText string
   273  	if g.HeaderFile != "" {
   274  		headerBytes, err := ctx.ReadFile(g.HeaderFile)
   275  		if err != nil {
   276  			return err
   277  		}
   278  		headerText = string(headerBytes)
   279  	}
   280  	headerText = strings.ReplaceAll(headerText, " YEAR", " "+g.Year)
   281  
   282  	return ctx.WriteYAML("role.yaml", headerText, objs, genall.WithTransform(genall.TransformRemoveCreationTimestamp))
   283  }