diff --git a/go.mod b/go.mod index 4bf4f6c12..d09d0afdd 100644 --- a/go.mod +++ b/go.mod @@ -18,9 +18,10 @@ require ( github.com/vmware-tanzu/carvel-kapp-controller v0.46.2 golang.org/x/net v0.20.0 gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.29.0 - k8s.io/apimachinery v0.29.0 - k8s.io/client-go v0.29.0 + k8s.io/api v0.29.1 + k8s.io/apimachinery v0.29.1 + k8s.io/client-go v0.29.1 + k8s.io/component-helpers v0.29.1 sigs.k8s.io/yaml v1.3.0 ) diff --git a/go.sum b/go.sum index 3abd414aa..150945f30 100644 --- a/go.sum +++ b/go.sum @@ -437,8 +437,8 @@ golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= -golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -509,12 +509,14 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= -k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= -k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= -k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= -k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= -k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= +k8s.io/api v0.29.1 h1:DAjwWX/9YT7NQD4INu49ROJuZAAAP/Ijki48GUPzxqw= +k8s.io/api v0.29.1/go.mod h1:7Kl10vBRUXhnQQI8YR/R327zXC8eJ7887/+Ybta+RoQ= +k8s.io/apimachinery v0.29.1 h1:KY4/E6km/wLBguvCZv8cKTeOwwOBqFNjwJIdMkMbbRc= +k8s.io/apimachinery v0.29.1/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/client-go v0.29.1 h1:19B/+2NGEwnFLzt0uB5kNJnfTsbV8w6TgQRz9l7ti7A= +k8s.io/client-go v0.29.1/go.mod h1:TDG/psL9hdet0TI9mGyHJSgRkW3H9JZk2dNEUS7bRks= +k8s.io/component-helpers v0.29.1 h1:54MMEDu6xeJmMtAKztsPwu0kJKr4+jCUzaEIn2UXRoc= +k8s.io/component-helpers v0.29.1/go.mod h1:+I7xz4kfUgxWAPJIVKrqe4ml4rb9UGpazlOmhXYo+cY= k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= diff --git a/pkg/kapp/cmd/app/deploy.go b/pkg/kapp/cmd/app/deploy.go index 0eac63d6e..83b12e915 100644 --- a/pkg/kapp/cmd/app/deploy.go +++ b/pkg/kapp/cmd/app/deploy.go @@ -4,6 +4,7 @@ package app import ( + "context" "fmt" "io/fs" "os" @@ -13,6 +14,7 @@ import ( "github.com/cppforlife/go-cli-ui/ui" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes" @@ -27,6 +29,7 @@ import ( ctldiffui "github.com/vmware-tanzu/carvel-kapp/pkg/kapp/diffui" "github.com/vmware-tanzu/carvel-kapp/pkg/kapp/logger" ctllogs "github.com/vmware-tanzu/carvel-kapp/pkg/kapp/logs" + "github.com/vmware-tanzu/carvel-kapp/pkg/kapp/permissions" ctlres "github.com/vmware-tanzu/carvel-kapp/pkg/kapp/resources" ctlresm "github.com/vmware-tanzu/carvel-kapp/pkg/kapp/resourcesmisc" ) @@ -172,6 +175,13 @@ func (o *DeployOptions) Run() error { return err } + if o.DeployFlags.ValidatePermissions { + err = o.validatePermissionsForChangeset(clusterChangesGraph) + if err != nil { + return err + } + } + // Validate new resources _after_ presenting changes to make it easier to see big picture err = prep.ValidateResources(newResources) if err != nil { @@ -572,3 +582,48 @@ func (o *DeployOptions) presentDiffUI(graph *ctldgraph.ChangeGraph) error { } return ctldiffui.NewServer(opts, o.ui).Run() } + +func (o *DeployOptions) validatePermissionsForChangeset(changeGraph *ctldgraph.ChangeGraph) error { + client, err := o.depsFactory.CoreClient() + if err != nil { + return err + } + + mapper, err := o.depsFactory.RESTMapper() + if err != nil { + return err + } + + roleValidator := permissions.NewRoleValidator(client.AuthorizationV1().SelfSubjectAccessReviews(), mapper) + basicValidator := permissions.NewBasicValidator(client.AuthorizationV1().SelfSubjectAccessReviews(), mapper) + + validator := permissions.NewCompositeValidator(basicValidator, map[schema.GroupVersionKind]permissions.Validator{ + rbacv1.SchemeGroupVersion.WithKind("Role"): roleValidator, + rbacv1.SchemeGroupVersion.WithKind("ClusterRole"): roleValidator, + }) + + for _, change := range changeGraph.All() { + switch change.Change.Op() { + case ctldgraph.ActualChangeOpDelete: + err = validator.Validate(context.Background(), change.Change.Resource(), "delete") + if err != nil { + return err + } + case ctldgraph.ActualChangeOpUpsert: + // Check both create and update permissions + err = validator.Validate(context.Background(), change.Change.Resource(), "create") + if err != nil { + return err + } + + err = validator.Validate(context.Background(), change.Change.Resource(), "update") + if err != nil { + return err + } + + // TODO: should check patch permissions? + } + } + + return nil +} diff --git a/pkg/kapp/cmd/app/deploy_flags.go b/pkg/kapp/cmd/app/deploy_flags.go index ba9a2ed9f..b518a1461 100644 --- a/pkg/kapp/cmd/app/deploy_flags.go +++ b/pkg/kapp/cmd/app/deploy_flags.go @@ -27,6 +27,8 @@ type DeployFlags struct { LogsAll bool AppMetadataFile string + ValidatePermissions bool + DisableGKScoping bool } @@ -60,4 +62,7 @@ func (s *DeployFlags) Set(cmd *cobra.Command) { cmd.Flags().BoolVar(&s.DisableGKScoping, "dangerous-disable-gk-scoping", false, "Disable scoping of resource searching to used GroupKinds") + + cmd.Flags().BoolVar(&s.ValidatePermissions, "validate-permissions", + false, "Validate permissions before attempting to apply changes to the cluster") } diff --git a/pkg/kapp/cmd/core/deps_factory.go b/pkg/kapp/cmd/core/deps_factory.go index 1f4955bc7..32c6515ec 100644 --- a/pkg/kapp/cmd/core/deps_factory.go +++ b/pkg/kapp/cmd/core/deps_factory.go @@ -9,15 +9,19 @@ import ( "sync" "github.com/cppforlife/go-cli-ui/ui" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "k8s.io/client-go/restmapper" ) type DepsFactory interface { DynamicClient(opts DynamicClientOpts) (dynamic.Interface, error) CoreClient() (kubernetes.Interface, error) + RESTMapper() (meta.RESTMapper, error) ConfigureWarnings(warnings bool) } @@ -83,6 +87,29 @@ func (f *DepsFactoryImpl) CoreClient() (kubernetes.Interface, error) { return clientset, nil } +func (f *DepsFactoryImpl) RESTMapper() (meta.RESTMapper, error) { + config, err := f.configFactory.RESTConfig() + if err != nil { + return nil, err + } + + disc, err := discovery.NewDiscoveryClientForConfig(config) + if err != nil { + return nil, err + } + + groupResources, err := restmapper.GetAPIGroupResources(disc) + if err != nil { + return nil, err + } + + mapper := restmapper.NewDiscoveryRESTMapper(groupResources) + + f.printTarget(config) + + return mapper, nil +} + func (f *DepsFactoryImpl) ConfigureWarnings(warnings bool) { f.Warnings = warnings } diff --git a/pkg/kapp/permissions/validator.go b/pkg/kapp/permissions/validator.go new file mode 100644 index 000000000..602379481 --- /dev/null +++ b/pkg/kapp/permissions/validator.go @@ -0,0 +1,508 @@ +package permissions + +import ( + "context" + "errors" + "fmt" + + ctlres "github.com/vmware-tanzu/carvel-kapp/pkg/kapp/resources" + authv1 "k8s.io/api/authorization/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/meta" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes" + authv1client "k8s.io/client-go/kubernetes/typed/authorization/v1" + "k8s.io/component-helpers/auth/rbac/validation" +) + +type Validator interface { + Validate(context.Context, ctlres.Resource, string) error +} + +var _ Validator = (*CompositeValidator)(nil) + +// CompositeValidator implements Validator and is used +// for composing multiple validators into a single validator +// that can handle specifying unique validators for different +// GroupVersionKinds +type CompositeValidator struct { + validators map[schema.GroupVersionKind]Validator + defaultValidator Validator +} + +func NewCompositeValidator(defaultValidator Validator, validators map[schema.GroupVersionKind]Validator) *CompositeValidator { + return &CompositeValidator{ + validators: validators, + defaultValidator: defaultValidator, + } +} + +func (cv *CompositeValidator) Validate(ctx context.Context, res ctlres.Resource, verb string) error { + if validator, ok := cv.validators[res.GroupVersion().WithKind(res.Kind())]; ok { + return validator.Validate(ctx, res, verb) + } + return cv.defaultValidator.Validate(ctx, res, verb) +} + +// BasicValidator is a basic validator useful for +// validating basic CRUD permissions for resources. It has no knowledge +// of how to handle permission evaluation for specific +// GroupVersionKinds +type BasicValidator struct { + ssarClient authv1client.SelfSubjectAccessReviewInterface + mapper meta.RESTMapper +} + +var _ Validator = (*BasicValidator)(nil) + +func NewBasicValidator(ssarClient authv1client.SelfSubjectAccessReviewInterface, mapper meta.RESTMapper) *BasicValidator { + return &BasicValidator{ + ssarClient: ssarClient, + mapper: mapper, + } +} + +func (bv *BasicValidator) Validate(ctx context.Context, res ctlres.Resource, verb string) error { + mapping, err := bv.mapper.RESTMapping(res.GroupKind(), res.GroupVersion().Version) + if err != nil { + return err + } + + ssar := &authv1.SelfSubjectAccessReview{ + Spec: authv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authv1.ResourceAttributes{ + Namespace: res.Namespace(), + Name: res.Name(), + Verb: verb, + Group: res.GroupVersion().Group, + Version: res.GroupVersion().Version, + Resource: mapping.Resource.Resource, + }, + }, + } + + retSsar, err := bv.ssarClient.Create(ctx, ssar, v1.CreateOptions{}) + if err != nil { + return err + } + + if retSsar == nil { + return errors.New("returned SelfSubjectAccessReview is nil...") + } + + if retSsar.Status.EvaluationError != "" { + return errors.New(retSsar.Status.EvaluationError) + } + + if !retSsar.Status.Allowed { + return fmt.Errorf("not permitted to %q %s, reason: %s", + verb, + mapping.Resource.String(), + retSsar.Status.Reason) + } + + return nil +} + +// RoleValidator is a Validator implementation +// for validating permissions required to CRUD +// Kubernetes Role resources +type RoleValidator struct { + ssarClient authv1client.SelfSubjectAccessReviewInterface + mapper meta.RESTMapper +} + +var _ Validator = (*RoleValidator)(nil) + +func NewRoleValidator(ssarClient authv1client.SelfSubjectAccessReviewInterface, mapper meta.RESTMapper) *RoleValidator { + return &RoleValidator{ + ssarClient: ssarClient, + mapper: mapper, + } +} + +func (rv *RoleValidator) Validate(ctx context.Context, res ctlres.Resource, verb string) error { + mapping, err := rv.mapper.RESTMapping(res.GroupKind(), res.GroupVersion().Version) + if err != nil { + return err + } + + switch verb { + case "create", "update": + // do early validation on create / update to see if a user has + // the "escalate" permissions which allows them to perform + // privilege escalation and create any (Cluster)Role + ssar := &authv1.SelfSubjectAccessReview{ + Spec: authv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authv1.ResourceAttributes{ + Namespace: res.Namespace(), + Verb: "escalate", + Group: res.GroupVersion().Group, + Version: res.GroupVersion().Version, + Resource: mapping.Resource.Resource, + }, + }, + } + + retSsar, err := rv.ssarClient.Create(ctx, ssar, v1.CreateOptions{}) + if err != nil { + return err + } + + if retSsar == nil { + return errors.New("returned SelfSubjectAccessReview is nil...") + } + + if retSsar.Status.EvaluationError != "" { + return errors.New(retSsar.Status.EvaluationError) + } + + if retSsar.Status.Allowed { + return nil + } + + // Check if user has permissions to even create/update the resource + ssar = &authv1.SelfSubjectAccessReview{ + Spec: authv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authv1.ResourceAttributes{ + Namespace: res.Namespace(), + Name: res.Name(), + Verb: verb, + Group: res.GroupVersion().Group, + Version: res.GroupVersion().Version, + Resource: mapping.Resource.Resource, + }, + }, + } + + retSsar, err = rv.ssarClient.Create(ctx, ssar, v1.CreateOptions{}) + if err != nil { + return err + } + + if retSsar == nil { + return errors.New("returned SelfSubjectAccessReview is nil...") + } + + if retSsar.Status.EvaluationError != "" { + return errors.New(retSsar.Status.EvaluationError) + } + + if !retSsar.Status.Allowed { + return fmt.Errorf("not permitted to %q %s, reason: %s", + verb, + mapping.Resource.String(), + retSsar.Status.Reason) + } + // If user doesn't have "escalate" permissions then they can + // only create (Cluster)Roles that contain permissions they already have. + // Loop through all the defined policies and determine + // if a user has the appropriate permissions + rules := []rbacv1.PolicyRule{} + switch res.Kind() { + case "Role": + role := &rbacv1.Role{} + err := res.AsTypedObj(role) + if err != nil { + return fmt.Errorf("converting resource to typed Role object: %w", err) + } + + rules = role.Rules + case "ClusterRole": + role := &rbacv1.ClusterRole{} + err := res.AsTypedObj(role) + if err != nil { + return fmt.Errorf("converting resource to typed Role object: %w", err) + } + + rules = role.Rules + default: + return fmt.Errorf("unknown role kind %q", res.Kind()) + } + + errorSet := []error{} + for _, rule := range rules { + // breakdown the rules into the subset of + // rules such that the subrules contain + // at most one verb, one group, and one resource + // source at: https://github.com/kubernetes/component-helpers/blob/9a5801419916272fc9cec7a7822ed525721b99d3/auth/rbac/validation/policy_comparator.go#L56-L84 + var subrules []rbacv1.PolicyRule = validation.BreakdownRule(rule) + for _, subrule := range subrules { + ssar := &authv1.SelfSubjectAccessReview{ + Spec: authv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authv1.ResourceAttributes{ + Namespace: res.Namespace(), + Verb: subrule.Verbs[0], + Group: subrule.APIGroups[0], + Resource: subrule.Resources[0], + }, + }, + } + + retSsar, err := rv.ssarClient.Create(ctx, ssar, v1.CreateOptions{}) + if err != nil { + return err + } + + if retSsar == nil { + return errors.New("returned SelfSubjectAccessReview is nil...") + } + + if retSsar.Status.EvaluationError != "" { + return errors.New(retSsar.Status.EvaluationError) + } + + if !retSsar.Status.Allowed { + gr := schema.GroupResource{ + Group: subrule.APIGroups[0], + Resource: subrule.Resources[0], + } + errorSet = append(errorSet, fmt.Errorf("missing %q permission for %s", subrule.Verbs[0], gr.String())) + } + } + } + + if len(errorSet) > 0 { + baseErr := fmt.Errorf("not permitted to %q %s:", verb, res.GroupVersion().WithKind(res.Kind()).String()) + return errors.Join(append([]error{baseErr}, errorSet...)...) + } + default: + ssar := &authv1.SelfSubjectAccessReview{ + Spec: authv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authv1.ResourceAttributes{ + Namespace: res.Namespace(), + Name: res.Name(), + Verb: verb, + Group: res.GroupVersion().Group, + Version: res.GroupVersion().Version, + Resource: mapping.Resource.Resource, + }, + }, + } + + retSsar, err := rv.ssarClient.Create(ctx, ssar, v1.CreateOptions{}) + if err != nil { + return err + } + + if retSsar == nil { + return errors.New("returned SelfSubjectAccessReview is nil...") + } + + if retSsar.Status.EvaluationError != "" { + return errors.New(retSsar.Status.EvaluationError) + } + + if !retSsar.Status.Allowed { + return fmt.Errorf("not permitted to %q %s, reason: %s", + verb, + mapping.Resource.String(), + retSsar.Status.Reason) + } + } + + return nil +} + +// BindingValidator is a Validator implementation +// for validating permissions required to CRUD +// Kubernetes RoleBinding resources +type BindingValidator struct { + ssarClient authv1client.SelfSubjectAccessReviewInterface + rbacClient + mapper meta.RESTMapper + roleValidator Validator +} + +var _ Validator = (*BindingValidator)(nil) + +func NewBindingValidator(ssarClient authv1client.SelfSubjectAccessReviewInterface, mapper meta.RESTMapper) *BindingValidator { + return &RoleValidator{ + ssarClient: ssarClient, + mapper: mapper, + } +} + +func (rv *RoleValidator) Validate(ctx context.Context, res ctlres.Resource, verb string) error { + mapping, err := rv.mapper.RESTMapping(res.GroupKind(), res.GroupVersion().Version) + if err != nil { + return err + } + + switch verb { + case "create", "update": + // do early validation on create / update to see if a user has + // the "escalate" permissions which allows them to perform + // privilege escalation and create any (Cluster)Role + ssar := &authv1.SelfSubjectAccessReview{ + Spec: authv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authv1.ResourceAttributes{ + Namespace: res.Namespace(), + Verb: "escalate", + Group: res.GroupVersion().Group, + Version: res.GroupVersion().Version, + Resource: mapping.Resource.Resource, + }, + }, + } + + retSsar, err := rv.ssarClient.Create(ctx, ssar, v1.CreateOptions{}) + if err != nil { + return err + } + + if retSsar == nil { + return errors.New("returned SelfSubjectAccessReview is nil...") + } + + if retSsar.Status.EvaluationError != "" { + return errors.New(retSsar.Status.EvaluationError) + } + + if retSsar.Status.Allowed { + return nil + } + + // Check if user has permissions to even create/update the resource + ssar = &authv1.SelfSubjectAccessReview{ + Spec: authv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authv1.ResourceAttributes{ + Namespace: res.Namespace(), + Name: res.Name(), + Verb: verb, + Group: res.GroupVersion().Group, + Version: res.GroupVersion().Version, + Resource: mapping.Resource.Resource, + }, + }, + } + + retSsar, err = rv.ssarClient.Create(ctx, ssar, v1.CreateOptions{}) + if err != nil { + return err + } + + if retSsar == nil { + return errors.New("returned SelfSubjectAccessReview is nil...") + } + + if retSsar.Status.EvaluationError != "" { + return errors.New(retSsar.Status.EvaluationError) + } + + if !retSsar.Status.Allowed { + return fmt.Errorf("not permitted to %q %s, reason: %s", + verb, + mapping.Resource.String(), + retSsar.Status.Reason) + } + // If user doesn't have "escalate" permissions then they can + // only create (Cluster)Roles that contain permissions they already have. + // Loop through all the defined policies and determine + // if a user has the appropriate permissions + rules := []rbacv1.PolicyRule{} + switch res.Kind() { + case "Role": + role := &rbacv1.Role{} + err := res.AsTypedObj(role) + if err != nil { + return fmt.Errorf("converting resource to typed Role object: %w", err) + } + + rules = role.Rules + case "ClusterRole": + role := &rbacv1.ClusterRole{} + err := res.AsTypedObj(role) + if err != nil { + return fmt.Errorf("converting resource to typed Role object: %w", err) + } + + rules = role.Rules + default: + return fmt.Errorf("unknown role kind %q", res.Kind()) + } + + errorSet := []error{} + for _, rule := range rules { + // breakdown the rules into the subset of + // rules such that the subrules contain + // at most one verb, one group, and one resource + // source at: https://github.com/kubernetes/component-helpers/blob/9a5801419916272fc9cec7a7822ed525721b99d3/auth/rbac/validation/policy_comparator.go#L56-L84 + var subrules []rbacv1.PolicyRule = validation.BreakdownRule(rule) + for _, subrule := range subrules { + ssar := &authv1.SelfSubjectAccessReview{ + Spec: authv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authv1.ResourceAttributes{ + Namespace: res.Namespace(), + Verb: subrule.Verbs[0], + Group: subrule.APIGroups[0], + Resource: subrule.Resources[0], + }, + }, + } + + retSsar, err := rv.ssarClient.Create(ctx, ssar, v1.CreateOptions{}) + if err != nil { + return err + } + + if retSsar == nil { + return errors.New("returned SelfSubjectAccessReview is nil...") + } + + if retSsar.Status.EvaluationError != "" { + return errors.New(retSsar.Status.EvaluationError) + } + + if !retSsar.Status.Allowed { + gr := schema.GroupResource{ + Group: subrule.APIGroups[0], + Resource: subrule.Resources[0], + } + errorSet = append(errorSet, fmt.Errorf("missing %q permission for %s", subrule.Verbs[0], gr.String())) + } + } + } + + if len(errorSet) > 0 { + baseErr := fmt.Errorf("not permitted to %q %s:", verb, res.GroupVersion().WithKind(res.Kind()).String()) + return errors.Join(append([]error{baseErr}, errorSet...)...) + } + default: + ssar := &authv1.SelfSubjectAccessReview{ + Spec: authv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authv1.ResourceAttributes{ + Namespace: res.Namespace(), + Name: res.Name(), + Verb: verb, + Group: res.GroupVersion().Group, + Version: res.GroupVersion().Version, + Resource: mapping.Resource.Resource, + }, + }, + } + + retSsar, err := rv.ssarClient.Create(ctx, ssar, v1.CreateOptions{}) + if err != nil { + return err + } + + if retSsar == nil { + return errors.New("returned SelfSubjectAccessReview is nil...") + } + + if retSsar.Status.EvaluationError != "" { + return errors.New(retSsar.Status.EvaluationError) + } + + if !retSsar.Status.Allowed { + return fmt.Errorf("not permitted to %q %s, reason: %s", + verb, + mapping.Resource.String(), + retSsar.Status.Reason) + } + } + + return nil +} diff --git a/vendor/k8s.io/client-go/restmapper/category_expansion.go b/vendor/k8s.io/client-go/restmapper/category_expansion.go new file mode 100644 index 000000000..484e4c839 --- /dev/null +++ b/vendor/k8s.io/client-go/restmapper/category_expansion.go @@ -0,0 +1,119 @@ +/* +Copyright 2017 The Kubernetes 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 restmapper + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" +) + +// CategoryExpander maps category strings to GroupResources. +// Categories are classification or 'tag' of a group of resources. +type CategoryExpander interface { + Expand(category string) ([]schema.GroupResource, bool) +} + +// SimpleCategoryExpander implements CategoryExpander interface +// using a static mapping of categories to GroupResource mapping. +type SimpleCategoryExpander struct { + Expansions map[string][]schema.GroupResource +} + +// Expand fulfills CategoryExpander +func (e SimpleCategoryExpander) Expand(category string) ([]schema.GroupResource, bool) { + ret, ok := e.Expansions[category] + return ret, ok +} + +// discoveryCategoryExpander struct lets a REST Client wrapper (discoveryClient) to retrieve list of APIResourceList, +// and then convert to fallbackExpander +type discoveryCategoryExpander struct { + discoveryClient discovery.DiscoveryInterface +} + +// NewDiscoveryCategoryExpander returns a category expander that makes use of the "categories" fields from +// the API, found through the discovery client. In case of any error or no category found (which likely +// means we're at a cluster prior to categories support, fallback to the expander provided. +func NewDiscoveryCategoryExpander(client discovery.DiscoveryInterface) CategoryExpander { + if client == nil { + panic("Please provide discovery client to shortcut expander") + } + return discoveryCategoryExpander{discoveryClient: client} +} + +// Expand fulfills CategoryExpander +func (e discoveryCategoryExpander) Expand(category string) ([]schema.GroupResource, bool) { + // Get all supported resources for groups and versions from server, if no resource found, fallback anyway. + _, apiResourceLists, _ := e.discoveryClient.ServerGroupsAndResources() + if len(apiResourceLists) == 0 { + return nil, false + } + + discoveredExpansions := map[string][]schema.GroupResource{} + for _, apiResourceList := range apiResourceLists { + gv, err := schema.ParseGroupVersion(apiResourceList.GroupVersion) + if err != nil { + continue + } + // Collect GroupVersions by categories + for _, apiResource := range apiResourceList.APIResources { + if categories := apiResource.Categories; len(categories) > 0 { + for _, category := range categories { + groupResource := schema.GroupResource{ + Group: gv.Group, + Resource: apiResource.Name, + } + discoveredExpansions[category] = append(discoveredExpansions[category], groupResource) + } + } + } + } + + ret, ok := discoveredExpansions[category] + return ret, ok +} + +// UnionCategoryExpander implements CategoryExpander interface. +// It maps given category string to union of expansions returned by all the CategoryExpanders in the list. +type UnionCategoryExpander []CategoryExpander + +// Expand fulfills CategoryExpander +func (u UnionCategoryExpander) Expand(category string) ([]schema.GroupResource, bool) { + ret := []schema.GroupResource{} + ok := false + + // Expand the category for each CategoryExpander in the list and merge/combine the results. + for _, expansion := range u { + curr, currOk := expansion.Expand(category) + + for _, currGR := range curr { + found := false + for _, existing := range ret { + if existing == currGR { + found = true + break + } + } + if !found { + ret = append(ret, currGR) + } + } + ok = ok || currOk + } + + return ret, ok +} diff --git a/vendor/k8s.io/client-go/restmapper/discovery.go b/vendor/k8s.io/client-go/restmapper/discovery.go new file mode 100644 index 000000000..3505178b6 --- /dev/null +++ b/vendor/k8s.io/client-go/restmapper/discovery.go @@ -0,0 +1,338 @@ +/* +Copyright 2016 The Kubernetes 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 restmapper + +import ( + "fmt" + "strings" + "sync" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + + "k8s.io/klog/v2" +) + +// APIGroupResources is an API group with a mapping of versions to +// resources. +type APIGroupResources struct { + Group metav1.APIGroup + // A mapping of version string to a slice of APIResources for + // that version. + VersionedResources map[string][]metav1.APIResource +} + +// NewDiscoveryRESTMapper returns a PriorityRESTMapper based on the discovered +// groups and resources passed in. +func NewDiscoveryRESTMapper(groupResources []*APIGroupResources) meta.RESTMapper { + unionMapper := meta.MultiRESTMapper{} + + var groupPriority []string + // /v1 is special. It should always come first + resourcePriority := []schema.GroupVersionResource{{Group: "", Version: "v1", Resource: meta.AnyResource}} + kindPriority := []schema.GroupVersionKind{{Group: "", Version: "v1", Kind: meta.AnyKind}} + + for _, group := range groupResources { + groupPriority = append(groupPriority, group.Group.Name) + + // Make sure the preferred version comes first + if len(group.Group.PreferredVersion.Version) != 0 { + preferred := group.Group.PreferredVersion.Version + if _, ok := group.VersionedResources[preferred]; ok { + resourcePriority = append(resourcePriority, schema.GroupVersionResource{ + Group: group.Group.Name, + Version: group.Group.PreferredVersion.Version, + Resource: meta.AnyResource, + }) + + kindPriority = append(kindPriority, schema.GroupVersionKind{ + Group: group.Group.Name, + Version: group.Group.PreferredVersion.Version, + Kind: meta.AnyKind, + }) + } + } + + for _, discoveryVersion := range group.Group.Versions { + resources, ok := group.VersionedResources[discoveryVersion.Version] + if !ok { + continue + } + + // Add non-preferred versions after the preferred version, in case there are resources that only exist in those versions + if discoveryVersion.Version != group.Group.PreferredVersion.Version { + resourcePriority = append(resourcePriority, schema.GroupVersionResource{ + Group: group.Group.Name, + Version: discoveryVersion.Version, + Resource: meta.AnyResource, + }) + + kindPriority = append(kindPriority, schema.GroupVersionKind{ + Group: group.Group.Name, + Version: discoveryVersion.Version, + Kind: meta.AnyKind, + }) + } + + gv := schema.GroupVersion{Group: group.Group.Name, Version: discoveryVersion.Version} + versionMapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{gv}) + + for _, resource := range resources { + scope := meta.RESTScopeNamespace + if !resource.Namespaced { + scope = meta.RESTScopeRoot + } + + // if we have a slash, then this is a subresource and we shouldn't create mappings for those. + if strings.Contains(resource.Name, "/") { + continue + } + + plural := gv.WithResource(resource.Name) + singular := gv.WithResource(resource.SingularName) + // this is for legacy resources and servers which don't list singular forms. For those we must still guess. + if len(resource.SingularName) == 0 { + _, singular = meta.UnsafeGuessKindToResource(gv.WithKind(resource.Kind)) + } + + versionMapper.AddSpecific(gv.WithKind(strings.ToLower(resource.Kind)), plural, singular, scope) + versionMapper.AddSpecific(gv.WithKind(resource.Kind), plural, singular, scope) + // TODO this is producing unsafe guesses that don't actually work, but it matches previous behavior + versionMapper.Add(gv.WithKind(resource.Kind+"List"), scope) + } + // TODO why is this type not in discovery (at least for "v1") + versionMapper.Add(gv.WithKind("List"), meta.RESTScopeRoot) + unionMapper = append(unionMapper, versionMapper) + } + } + + for _, group := range groupPriority { + resourcePriority = append(resourcePriority, schema.GroupVersionResource{ + Group: group, + Version: meta.AnyVersion, + Resource: meta.AnyResource, + }) + kindPriority = append(kindPriority, schema.GroupVersionKind{ + Group: group, + Version: meta.AnyVersion, + Kind: meta.AnyKind, + }) + } + + return meta.PriorityRESTMapper{ + Delegate: unionMapper, + ResourcePriority: resourcePriority, + KindPriority: kindPriority, + } +} + +// GetAPIGroupResources uses the provided discovery client to gather +// discovery information and populate a slice of APIGroupResources. +func GetAPIGroupResources(cl discovery.DiscoveryInterface) ([]*APIGroupResources, error) { + gs, rs, err := cl.ServerGroupsAndResources() + if rs == nil || gs == nil { + return nil, err + // TODO track the errors and update callers to handle partial errors. + } + rsm := map[string]*metav1.APIResourceList{} + for _, r := range rs { + rsm[r.GroupVersion] = r + } + + var result []*APIGroupResources + for _, group := range gs { + groupResources := &APIGroupResources{ + Group: *group, + VersionedResources: make(map[string][]metav1.APIResource), + } + for _, version := range group.Versions { + resources, ok := rsm[version.GroupVersion] + if !ok { + continue + } + groupResources.VersionedResources[version.Version] = resources.APIResources + } + result = append(result, groupResources) + } + return result, nil +} + +// DeferredDiscoveryRESTMapper is a RESTMapper that will defer +// initialization of the RESTMapper until the first mapping is +// requested. +type DeferredDiscoveryRESTMapper struct { + initMu sync.Mutex + delegate meta.RESTMapper + cl discovery.CachedDiscoveryInterface +} + +// NewDeferredDiscoveryRESTMapper returns a +// DeferredDiscoveryRESTMapper that will lazily query the provided +// client for discovery information to do REST mappings. +func NewDeferredDiscoveryRESTMapper(cl discovery.CachedDiscoveryInterface) *DeferredDiscoveryRESTMapper { + return &DeferredDiscoveryRESTMapper{ + cl: cl, + } +} + +func (d *DeferredDiscoveryRESTMapper) getDelegate() (meta.RESTMapper, error) { + d.initMu.Lock() + defer d.initMu.Unlock() + + if d.delegate != nil { + return d.delegate, nil + } + + groupResources, err := GetAPIGroupResources(d.cl) + if err != nil { + return nil, err + } + + d.delegate = NewDiscoveryRESTMapper(groupResources) + return d.delegate, nil +} + +// Reset resets the internally cached Discovery information and will +// cause the next mapping request to re-discover. +func (d *DeferredDiscoveryRESTMapper) Reset() { + klog.V(5).Info("Invalidating discovery information") + + d.initMu.Lock() + defer d.initMu.Unlock() + + d.cl.Invalidate() + d.delegate = nil +} + +// KindFor takes a partial resource and returns back the single match. +// It returns an error if there are multiple matches. +func (d *DeferredDiscoveryRESTMapper) KindFor(resource schema.GroupVersionResource) (gvk schema.GroupVersionKind, err error) { + del, err := d.getDelegate() + if err != nil { + return schema.GroupVersionKind{}, err + } + gvk, err = del.KindFor(resource) + if err != nil && !d.cl.Fresh() { + d.Reset() + gvk, err = d.KindFor(resource) + } + return +} + +// KindsFor takes a partial resource and returns back the list of +// potential kinds in priority order. +func (d *DeferredDiscoveryRESTMapper) KindsFor(resource schema.GroupVersionResource) (gvks []schema.GroupVersionKind, err error) { + del, err := d.getDelegate() + if err != nil { + return nil, err + } + gvks, err = del.KindsFor(resource) + if len(gvks) == 0 && !d.cl.Fresh() { + d.Reset() + gvks, err = d.KindsFor(resource) + } + return +} + +// ResourceFor takes a partial resource and returns back the single +// match. It returns an error if there are multiple matches. +func (d *DeferredDiscoveryRESTMapper) ResourceFor(input schema.GroupVersionResource) (gvr schema.GroupVersionResource, err error) { + del, err := d.getDelegate() + if err != nil { + return schema.GroupVersionResource{}, err + } + gvr, err = del.ResourceFor(input) + if err != nil && !d.cl.Fresh() { + d.Reset() + gvr, err = d.ResourceFor(input) + } + return +} + +// ResourcesFor takes a partial resource and returns back the list of +// potential resource in priority order. +func (d *DeferredDiscoveryRESTMapper) ResourcesFor(input schema.GroupVersionResource) (gvrs []schema.GroupVersionResource, err error) { + del, err := d.getDelegate() + if err != nil { + return nil, err + } + gvrs, err = del.ResourcesFor(input) + if len(gvrs) == 0 && !d.cl.Fresh() { + d.Reset() + gvrs, err = d.ResourcesFor(input) + } + return +} + +// RESTMapping identifies a preferred resource mapping for the +// provided group kind. +func (d *DeferredDiscoveryRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (m *meta.RESTMapping, err error) { + del, err := d.getDelegate() + if err != nil { + return nil, err + } + m, err = del.RESTMapping(gk, versions...) + if err != nil && !d.cl.Fresh() { + d.Reset() + m, err = d.RESTMapping(gk, versions...) + } + return +} + +// RESTMappings returns the RESTMappings for the provided group kind +// in a rough internal preferred order. If no kind is found, it will +// return a NoResourceMatchError. +func (d *DeferredDiscoveryRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) (ms []*meta.RESTMapping, err error) { + del, err := d.getDelegate() + if err != nil { + return nil, err + } + ms, err = del.RESTMappings(gk, versions...) + if len(ms) == 0 && !d.cl.Fresh() { + d.Reset() + ms, err = d.RESTMappings(gk, versions...) + } + return +} + +// ResourceSingularizer converts a resource name from plural to +// singular (e.g., from pods to pod). +func (d *DeferredDiscoveryRESTMapper) ResourceSingularizer(resource string) (singular string, err error) { + del, err := d.getDelegate() + if err != nil { + return resource, err + } + singular, err = del.ResourceSingularizer(resource) + if err != nil && !d.cl.Fresh() { + d.Reset() + singular, err = d.ResourceSingularizer(resource) + } + return +} + +func (d *DeferredDiscoveryRESTMapper) String() string { + del, err := d.getDelegate() + if err != nil { + return fmt.Sprintf("DeferredDiscoveryRESTMapper{%v}", err) + } + return fmt.Sprintf("DeferredDiscoveryRESTMapper{\n\t%v\n}", del) +} + +// Make sure it satisfies the interface +var _ meta.ResettableRESTMapper = &DeferredDiscoveryRESTMapper{} diff --git a/vendor/k8s.io/client-go/restmapper/shortcut.go b/vendor/k8s.io/client-go/restmapper/shortcut.go new file mode 100644 index 000000000..ca517a01d --- /dev/null +++ b/vendor/k8s.io/client-go/restmapper/shortcut.go @@ -0,0 +1,211 @@ +/* +Copyright 2016 The Kubernetes 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 restmapper + +import ( + "fmt" + "strings" + + "k8s.io/klog/v2" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" +) + +// shortcutExpander is a RESTMapper that can be used for Kubernetes resources. It expands the resource first, then invokes the wrapped +type shortcutExpander struct { + RESTMapper meta.RESTMapper + + discoveryClient discovery.DiscoveryInterface + + warningHandler func(string) +} + +var _ meta.ResettableRESTMapper = shortcutExpander{} + +// NewShortcutExpander wraps a restmapper in a layer that expands shortcuts found via discovery +func NewShortcutExpander(delegate meta.RESTMapper, client discovery.DiscoveryInterface, warningHandler func(string)) meta.RESTMapper { + return shortcutExpander{RESTMapper: delegate, discoveryClient: client, warningHandler: warningHandler} +} + +// KindFor fulfills meta.RESTMapper +func (e shortcutExpander) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) { + // expandResourceShortcut works with current API resources as read from discovery cache. + // In case of new CRDs this means we potentially don't have current state of discovery. + // In the current wiring in k8s.io/cli-runtime/pkg/genericclioptions/config_flags.go#toRESTMapper, + // we are using DeferredDiscoveryRESTMapper which on KindFor failure will clear the + // cache and fetch all data from a cluster (see vendor/k8s.io/client-go/restmapper/discovery.go#KindFor). + // Thus another call to expandResourceShortcut, after a NoMatchError should successfully + // read Kind to the user or an error. + gvk, err := e.RESTMapper.KindFor(e.expandResourceShortcut(resource)) + if meta.IsNoMatchError(err) { + return e.RESTMapper.KindFor(e.expandResourceShortcut(resource)) + } + return gvk, err +} + +// KindsFor fulfills meta.RESTMapper +func (e shortcutExpander) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) { + return e.RESTMapper.KindsFor(e.expandResourceShortcut(resource)) +} + +// ResourcesFor fulfills meta.RESTMapper +func (e shortcutExpander) ResourcesFor(resource schema.GroupVersionResource) ([]schema.GroupVersionResource, error) { + return e.RESTMapper.ResourcesFor(e.expandResourceShortcut(resource)) +} + +// ResourceFor fulfills meta.RESTMapper +func (e shortcutExpander) ResourceFor(resource schema.GroupVersionResource) (schema.GroupVersionResource, error) { + return e.RESTMapper.ResourceFor(e.expandResourceShortcut(resource)) +} + +// ResourceSingularizer fulfills meta.RESTMapper +func (e shortcutExpander) ResourceSingularizer(resource string) (string, error) { + return e.RESTMapper.ResourceSingularizer(e.expandResourceShortcut(schema.GroupVersionResource{Resource: resource}).Resource) +} + +// RESTMapping fulfills meta.RESTMapper +func (e shortcutExpander) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) { + return e.RESTMapper.RESTMapping(gk, versions...) +} + +// RESTMappings fulfills meta.RESTMapper +func (e shortcutExpander) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) { + return e.RESTMapper.RESTMappings(gk, versions...) +} + +// getShortcutMappings returns a set of tuples which holds short names for resources. +// First the list of potential resources will be taken from the API server. +// Next we will append the hardcoded list of resources - to be backward compatible with old servers. +// NOTE that the list is ordered by group priority. +func (e shortcutExpander) getShortcutMappings() ([]*metav1.APIResourceList, []resourceShortcuts, error) { + res := []resourceShortcuts{} + // get server resources + // This can return an error *and* the results it was able to find. We don't need to fail on the error. + _, apiResList, err := e.discoveryClient.ServerGroupsAndResources() + if err != nil { + klog.V(1).Infof("Error loading discovery information: %v", err) + } + for _, apiResources := range apiResList { + gv, err := schema.ParseGroupVersion(apiResources.GroupVersion) + if err != nil { + klog.V(1).Infof("Unable to parse groupversion = %s due to = %s", apiResources.GroupVersion, err.Error()) + continue + } + for _, apiRes := range apiResources.APIResources { + for _, shortName := range apiRes.ShortNames { + rs := resourceShortcuts{ + ShortForm: schema.GroupResource{Group: gv.Group, Resource: shortName}, + LongForm: schema.GroupResource{Group: gv.Group, Resource: apiRes.Name}, + } + res = append(res, rs) + } + } + } + + return apiResList, res, nil +} + +// expandResourceShortcut will return the expanded version of resource +// (something that a pkg/api/meta.RESTMapper can understand), if it is +// indeed a shortcut. If no match has been found, we will match on group prefixing. +// Lastly we will return resource unmodified. +func (e shortcutExpander) expandResourceShortcut(resource schema.GroupVersionResource) schema.GroupVersionResource { + // get the shortcut mappings and return on first match. + if allResources, shortcutResources, err := e.getShortcutMappings(); err == nil { + // avoid expanding if there's an exact match to a full resource name + for _, apiResources := range allResources { + gv, err := schema.ParseGroupVersion(apiResources.GroupVersion) + if err != nil { + continue + } + if len(resource.Group) != 0 && resource.Group != gv.Group { + continue + } + for _, apiRes := range apiResources.APIResources { + if resource.Resource == apiRes.Name { + return resource + } + if resource.Resource == apiRes.SingularName { + return resource + } + } + } + + found := false + var rsc schema.GroupVersionResource + warnedAmbiguousShortcut := make(map[schema.GroupResource]bool) + for _, item := range shortcutResources { + if len(resource.Group) != 0 && resource.Group != item.ShortForm.Group { + continue + } + if resource.Resource == item.ShortForm.Resource { + if found { + if item.LongForm.Group == rsc.Group && item.LongForm.Resource == rsc.Resource { + // It is common and acceptable that group/resource has multiple + // versions registered in cluster. This does not introduce ambiguity + // in terms of shortname usage. + continue + } + if !warnedAmbiguousShortcut[item.LongForm] { + if e.warningHandler != nil { + e.warningHandler(fmt.Sprintf("short name %q could also match lower priority resource %s", resource.Resource, item.LongForm.String())) + } + warnedAmbiguousShortcut[item.LongForm] = true + } + continue + } + rsc.Resource = item.LongForm.Resource + rsc.Group = item.LongForm.Group + found = true + } + } + if found { + return rsc + } + + // we didn't find exact match so match on group prefixing. This allows autoscal to match autoscaling + if len(resource.Group) == 0 { + return resource + } + for _, item := range shortcutResources { + if !strings.HasPrefix(item.ShortForm.Group, resource.Group) { + continue + } + if resource.Resource == item.ShortForm.Resource { + resource.Resource = item.LongForm.Resource + resource.Group = item.LongForm.Group + return resource + } + } + } + + return resource +} + +func (e shortcutExpander) Reset() { + meta.MaybeResetRESTMapper(e.RESTMapper) +} + +// ResourceShortcuts represents a structure that holds the information how to +// transition from resource's shortcut to its full name. +type resourceShortcuts struct { + ShortForm schema.GroupResource + LongForm schema.GroupResource +} diff --git a/vendor/k8s.io/component-helpers/LICENSE b/vendor/k8s.io/component-helpers/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/vendor/k8s.io/component-helpers/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/vendor/k8s.io/component-helpers/auth/rbac/validation/policy_comparator.go b/vendor/k8s.io/component-helpers/auth/rbac/validation/policy_comparator.go new file mode 100644 index 000000000..7a0268b5e --- /dev/null +++ b/vendor/k8s.io/component-helpers/auth/rbac/validation/policy_comparator.go @@ -0,0 +1,173 @@ +/* +Copyright 2016 The Kubernetes 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 validation + +import ( + "strings" + + rbacv1 "k8s.io/api/rbac/v1" +) + +// Covers determines whether or not the ownerRules cover the servantRules in terms of allowed actions. +// It returns whether or not the ownerRules cover and a list of the rules that the ownerRules do not cover. +func Covers(ownerRules, servantRules []rbacv1.PolicyRule) (bool, []rbacv1.PolicyRule) { + // 1. Break every servantRule into individual rule tuples: group, verb, resource, resourceName + // 2. Compare the mini-rules against each owner rule. Because the breakdown is down to the most atomic level, we're guaranteed that each mini-servant rule will be either fully covered or not covered by a single owner rule + // 3. Any left over mini-rules means that we are not covered and we have a nice list of them. + // TODO: it might be nice to collapse the list down into something more human readable + + subrules := []rbacv1.PolicyRule{} + for _, servantRule := range servantRules { + subrules = append(subrules, BreakdownRule(servantRule)...) + } + + uncoveredRules := []rbacv1.PolicyRule{} + for _, subrule := range subrules { + covered := false + for _, ownerRule := range ownerRules { + if ruleCovers(ownerRule, subrule) { + covered = true + break + } + } + + if !covered { + uncoveredRules = append(uncoveredRules, subrule) + } + } + + return (len(uncoveredRules) == 0), uncoveredRules +} + +// BreadownRule takes a rule and builds an equivalent list of rules that each have at most one verb, one +// resource, and one resource name +func BreakdownRule(rule rbacv1.PolicyRule) []rbacv1.PolicyRule { + subrules := []rbacv1.PolicyRule{} + for _, group := range rule.APIGroups { + for _, resource := range rule.Resources { + for _, verb := range rule.Verbs { + if len(rule.ResourceNames) > 0 { + for _, resourceName := range rule.ResourceNames { + subrules = append(subrules, rbacv1.PolicyRule{APIGroups: []string{group}, Resources: []string{resource}, Verbs: []string{verb}, ResourceNames: []string{resourceName}}) + } + + } else { + subrules = append(subrules, rbacv1.PolicyRule{APIGroups: []string{group}, Resources: []string{resource}, Verbs: []string{verb}}) + } + + } + } + } + + // Non-resource URLs are unique because they only combine with verbs. + for _, nonResourceURL := range rule.NonResourceURLs { + for _, verb := range rule.Verbs { + subrules = append(subrules, rbacv1.PolicyRule{NonResourceURLs: []string{nonResourceURL}, Verbs: []string{verb}}) + } + } + + return subrules +} + +func has(set []string, ele string) bool { + for _, s := range set { + if s == ele { + return true + } + } + return false +} + +func hasAll(set, contains []string) bool { + owning := make(map[string]struct{}, len(set)) + for _, ele := range set { + owning[ele] = struct{}{} + } + for _, ele := range contains { + if _, ok := owning[ele]; !ok { + return false + } + } + return true +} + +func resourceCoversAll(setResources, coversResources []string) bool { + // if we have a star or an exact match on all resources, then we match + if has(setResources, rbacv1.ResourceAll) || hasAll(setResources, coversResources) { + return true + } + + for _, path := range coversResources { + // if we have an exact match, then we match. + if has(setResources, path) { + continue + } + // if we're not a subresource, then we definitely don't match. fail. + if !strings.Contains(path, "/") { + return false + } + tokens := strings.SplitN(path, "/", 2) + resourceToCheck := "*/" + tokens[1] + if !has(setResources, resourceToCheck) { + return false + } + } + + return true +} + +func nonResourceURLsCoversAll(set, covers []string) bool { + for _, path := range covers { + covered := false + for _, owner := range set { + if nonResourceURLCovers(owner, path) { + covered = true + break + } + } + if !covered { + return false + } + } + return true +} + +func nonResourceURLCovers(ownerPath, subPath string) bool { + if ownerPath == subPath { + return true + } + return strings.HasSuffix(ownerPath, "*") && strings.HasPrefix(subPath, strings.TrimRight(ownerPath, "*")) +} + +// ruleCovers determines whether the ownerRule (which may have multiple verbs, resources, and resourceNames) covers +// the subrule (which may only contain at most one verb, resource, and resourceName) +func ruleCovers(ownerRule, subRule rbacv1.PolicyRule) bool { + verbMatches := has(ownerRule.Verbs, rbacv1.VerbAll) || hasAll(ownerRule.Verbs, subRule.Verbs) + groupMatches := has(ownerRule.APIGroups, rbacv1.APIGroupAll) || hasAll(ownerRule.APIGroups, subRule.APIGroups) + resourceMatches := resourceCoversAll(ownerRule.Resources, subRule.Resources) + nonResourceURLMatches := nonResourceURLsCoversAll(ownerRule.NonResourceURLs, subRule.NonResourceURLs) + + resourceNameMatches := false + + if len(subRule.ResourceNames) == 0 { + resourceNameMatches = (len(ownerRule.ResourceNames) == 0) + } else { + resourceNameMatches = (len(ownerRule.ResourceNames) == 0) || hasAll(ownerRule.ResourceNames, subRule.ResourceNames) + } + + return verbMatches && groupMatches && resourceMatches && resourceNameMatches && nonResourceURLMatches +} diff --git a/vendor/modules.txt b/vendor/modules.txt index be00c100f..93d07fe60 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -237,7 +237,7 @@ gopkg.in/yaml.v2 # gopkg.in/yaml.v3 v3.0.1 ## explicit gopkg.in/yaml.v3 -# k8s.io/api v0.29.0 +# k8s.io/api v0.29.1 ## explicit; go 1.21 k8s.io/api/admissionregistration/v1 k8s.io/api/admissionregistration/v1alpha1 @@ -291,7 +291,7 @@ k8s.io/api/scheduling/v1beta1 k8s.io/api/storage/v1 k8s.io/api/storage/v1alpha1 k8s.io/api/storage/v1beta1 -# k8s.io/apimachinery v0.29.0 +# k8s.io/apimachinery v0.29.1 ## explicit; go 1.21 k8s.io/apimachinery/pkg/api/equality k8s.io/apimachinery/pkg/api/errors @@ -334,7 +334,7 @@ k8s.io/apimachinery/pkg/util/yaml k8s.io/apimachinery/pkg/version k8s.io/apimachinery/pkg/watch k8s.io/apimachinery/third_party/forked/golang/reflect -# k8s.io/client-go v0.29.0 +# k8s.io/client-go v0.29.1 ## explicit; go 1.21 k8s.io/client-go/applyconfigurations/admissionregistration/v1 k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1 @@ -452,6 +452,7 @@ k8s.io/client-go/plugin/pkg/client/auth/gcp k8s.io/client-go/plugin/pkg/client/auth/oidc k8s.io/client-go/rest k8s.io/client-go/rest/watch +k8s.io/client-go/restmapper k8s.io/client-go/tools/auth k8s.io/client-go/tools/clientcmd k8s.io/client-go/tools/clientcmd/api @@ -466,6 +467,9 @@ k8s.io/client-go/util/flowcontrol k8s.io/client-go/util/homedir k8s.io/client-go/util/keyutil k8s.io/client-go/util/workqueue +# k8s.io/component-helpers v0.29.1 +## explicit; go 1.21 +k8s.io/component-helpers/auth/rbac/validation # k8s.io/klog/v2 v2.110.1 ## explicit; go 1.13 k8s.io/klog/v2