diff --git a/.golangci.yaml b/.golangci.yaml index 7f64bc040..cf46e72fd 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -16,6 +16,10 @@ run: # Default timeout is 1m, up to give more room timeout: 4m +issues: + exclude-dirs: + - internal/operator-controller/authorization/internal/kubernetes + linters: enable: - asciicheck diff --git a/Makefile b/Makefile index 5999aad47..ff39ee06a 100644 --- a/Makefile +++ b/Makefile @@ -135,7 +135,7 @@ manifests: $(CONTROLLER_GEN) #EXHELP Generate WebhookConfiguration, ClusterRole, .PHONY: generate generate: $(CONTROLLER_GEN) #EXHELP Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. - $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./api/..." .PHONY: verify verify: tidy fmt generate manifests crd-ref-docs #HELP Verify all generated code is up-to-date. diff --git a/api/v1/clusterextension_types.go b/api/v1/clusterextension_types.go index 0141f1a7a..abf0bf7b3 100644 --- a/api/v1/clusterextension_types.go +++ b/api/v1/clusterextension_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1 import ( + rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -454,6 +455,55 @@ type ClusterExtensionStatus struct { // // +optional Install *ClusterExtensionInstallStatus `json:"install,omitempty"` + + // rules is a representation of an assessment of permissions related + // to the cluster extension and its service account. + // + // +optional + Rules *ClusterExtensionRulesStatus `json:"rules,omitempty"` +} + +type ClusterExtensionRulesStatus struct { + // missing is the group of rules that the cluster extension's service account is likely lacking, + // but which are needed to be able to fully manage all resources in the resolved cluster extension manifest. + // + // +listType=atomic + // +optional + // +kubebuilder:validation:MaxItems:=64 + Missing []ClusterExtensionRulesStatusItem `json:"missing,omitempty"` +} + +type ClusterExtensionRulesStatusItem struct { + // namespace is a reference to a Kubernetes namespace. + // This is the namespace where the ClusterExtension's service account is lacking + // the rules provided in missingRules. + // + // When namespace is non-empty, one or more RoleBindings should be created to reference + // Roles and/or ClusterRoles that provided the missingRules in the namespace. + // + // When namespace is the empty string, one or more ClusterRoleBindings should be created + // to reference ClusterRoles that provide the missingRules cluster-wide. + // + // namespace is required and follows the DNS label standard + // as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters or hyphens (-), + // start and end with an alphanumeric character, and be no longer than 63 characters + // + // [RFC 1123]: https://tools.ietf.org/html/rfc1123 + // + // +kubebuilder:validation:MaxLength:=63 + // +kubebuilder:validation:XValidation:rule="self == \"\" || self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$\")",message="namespace must be empty string or a valid DNS1123 label" + // +kubebuilder:validation:Required + Namespace string `json:"namespace"` + + // rules is a list of RBAC policy rules that are related to the management of the rendered manifest of the + // ClusterExtension's bundle. + // + // rules is optional. If it is empty or nil, the semantic meaning is that there are no rules in this grouping + // for the named namespace. + // + // +listType=atomic + // +kubebuilder:validation:MaxItems:=1024 + Rules []rbacv1.PolicyRule `json:"rules,omitempty"` } // ClusterExtensionInstallStatus is a representation of the status of the identified bundle. diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 37694f61f..7f3a092ea 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1 import ( + rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -321,6 +322,50 @@ func (in *ClusterExtensionList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRulesStatus) DeepCopyInto(out *ClusterExtensionRulesStatus) { + *out = *in + if in.Missing != nil { + in, out := &in.Missing, &out.Missing + *out = make([]ClusterExtensionRulesStatusItem, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRulesStatus. +func (in *ClusterExtensionRulesStatus) DeepCopy() *ClusterExtensionRulesStatus { + if in == nil { + return nil + } + out := new(ClusterExtensionRulesStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRulesStatusItem) DeepCopyInto(out *ClusterExtensionRulesStatusItem) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]rbacv1.PolicyRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRulesStatusItem. +func (in *ClusterExtensionRulesStatusItem) DeepCopy() *ClusterExtensionRulesStatusItem { + if in == nil { + return nil + } + out := new(ClusterExtensionRulesStatusItem) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterExtensionSpec) DeepCopyInto(out *ClusterExtensionSpec) { *out = *in @@ -358,6 +403,11 @@ func (in *ClusterExtensionStatus) DeepCopyInto(out *ClusterExtensionStatus) { *out = new(ClusterExtensionInstallStatus) **out = **in } + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = new(ClusterExtensionRulesStatus) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionStatus. diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index ee6450a05..916d4ae54 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -32,6 +32,7 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" "k8s.io/apimachinery/pkg/fields" k8slabels "k8s.io/apimachinery/pkg/labels" @@ -58,6 +59,7 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/action" "github.com/operator-framework/operator-controller/internal/operator-controller/applier" "github.com/operator-framework/operator-controller/internal/operator-controller/authentication" + "github.com/operator-framework/operator-controller/internal/operator-controller/authorization" "github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/cache" catalogclient "github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/client" "github.com/operator-framework/operator-controller/internal/operator-controller/contentmanager" @@ -201,8 +203,12 @@ func run() error { setupLog.Info("set up manager") cacheOptions := crcache.Options{ ByObject: map[client.Object]crcache.ByObject{ - &ocv1.ClusterExtension{}: {Label: k8slabels.Everything()}, - &ocv1.ClusterCatalog{}: {Label: k8slabels.Everything()}, + &ocv1.ClusterExtension{}: {Label: k8slabels.Everything()}, + &ocv1.ClusterCatalog{}: {Label: k8slabels.Everything()}, + &rbacv1.ClusterRole{}: {Label: k8slabels.Everything()}, + &rbacv1.ClusterRoleBinding{}: {Label: k8slabels.Everything()}, + &rbacv1.Role{}: {Namespaces: map[string]crcache.Config{}, Label: k8slabels.Everything()}, + &rbacv1.RoleBinding{}: {Namespaces: map[string]crcache.Config{}, Label: k8slabels.Everything()}, }, DefaultNamespaces: map[string]crcache.Config{ cfg.systemNamespace: {LabelSelector: k8slabels.Everything()}, @@ -409,6 +415,7 @@ func run() error { helmApplier := &applier.Helm{ ActionClientGetter: acg, Preflights: preflights, + PreAuthorizer: authorization.NewRBACPreAuthorizer(mgr.GetClient()), } cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper()) diff --git a/codecov.yml b/codecov.yml index a3bfabd61..5370b84e1 100644 --- a/codecov.yml +++ b/codecov.yml @@ -8,4 +8,7 @@ coverage: paths: - "api/" - "cmd/" - - "internal/" \ No newline at end of file + - "internal/" + +ignore: + - "internal/operator-controller/authorization/internal/kubernetes" diff --git a/config/base/operator-controller/crd/bases/olm.operatorframework.io_clusterextensions.yaml b/config/base/operator-controller/crd/bases/olm.operatorframework.io_clusterextensions.yaml index e54b68518..3f8516751 100644 --- a/config/base/operator-controller/crd/bases/olm.operatorframework.io_clusterextensions.yaml +++ b/config/base/operator-controller/crd/bases/olm.operatorframework.io_clusterextensions.yaml @@ -581,6 +581,105 @@ spec: required: - bundle type: object + rules: + description: |- + rules is a representation of an assessment of permissions related + to the cluster extension and its service account. + properties: + missing: + description: |- + missing is the group of rules that the cluster extension's service account is likely lacking, + but which are needed to be able to fully manage all resources in the resolved cluster extension manifest. + items: + properties: + namespace: + description: |- + namespace is a reference to a Kubernetes namespace. + This is the namespace where the ClusterExtension's service account is lacking + the rules provided in missingRules. + + When namespace is non-empty, one or more RoleBindings should be created to reference + Roles and/or ClusterRoles that provided the missingRules in the namespace. + + When namespace is the empty string, one or more ClusterRoleBindings should be created + to reference ClusterRoles that provide the missingRules cluster-wide. + + namespace is required and follows the DNS label standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters or hyphens (-), + start and end with an alphanumeric character, and be no longer than 63 characters + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 63 + type: string + x-kubernetes-validations: + - message: namespace must be empty string or a valid DNS1123 + label + rule: self == "" || self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + rules: + description: |- + rules is a list of RBAC policy rules that are related to the management of the rendered manifest of the + ClusterExtension's bundle. + + rules is optional. If it is empty or nil, the semantic meaning is that there are no rules in this grouping + for the named namespace. + items: + description: |- + PolicyRule holds information that describes a policy rule, but does not contain information + about who the rule applies to or which namespace the rule applies to. + properties: + apiGroups: + description: |- + APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of + the enumerated resources in any API group will be allowed. "" represents the core API group and "*" represents all API groups. + items: + type: string + type: array + x-kubernetes-list-type: atomic + nonResourceURLs: + description: |- + NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path + Since non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding. + Rules can either apply to API resources (such as "pods" or "secrets") or non-resource URL paths (such as "/api"), but not both. + items: + type: string + type: array + x-kubernetes-list-type: atomic + resourceNames: + description: ResourceNames is an optional white list + of names that the rule applies to. An empty set + means that everything is allowed. + items: + type: string + type: array + x-kubernetes-list-type: atomic + resources: + description: Resources is a list of resources this + rule applies to. '*' represents all resources. + items: + type: string + type: array + x-kubernetes-list-type: atomic + verbs: + description: Verbs is a list of Verbs that apply to + ALL the ResourceKinds contained in this rule. '*' + represents all verbs. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - verbs + type: object + maxItems: 1024 + type: array + x-kubernetes-list-type: atomic + required: + - namespace + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + type: object type: object type: object served: true diff --git a/config/base/operator-controller/rbac/role.yaml b/config/base/operator-controller/rbac/role.yaml index a929e78e9..be89deec1 100644 --- a/config/base/operator-controller/rbac/role.yaml +++ b/config/base/operator-controller/rbac/role.yaml @@ -47,6 +47,16 @@ rules: verbs: - patch - update +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - list + - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role diff --git a/docs/api-reference/operator-controller-api-reference.md b/docs/api-reference/operator-controller-api-reference.md index 84fdbfa64..936086d5d 100644 --- a/docs/api-reference/operator-controller-api-reference.md +++ b/docs/api-reference/operator-controller-api-reference.md @@ -292,6 +292,39 @@ ClusterExtensionList contains a list of ClusterExtension | `items` _[ClusterExtension](#clusterextension) array_ | items is a required list of ClusterExtension objects. | | Required: \{\}
| +#### ClusterExtensionRulesStatus + + + + + + + +_Appears in:_ +- [ClusterExtensionStatus](#clusterextensionstatus) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `missing` _[ClusterExtensionRulesStatusItem](#clusterextensionrulesstatusitem) array_ | missing is the group of rules that the cluster extension's service account is likely lacking,
but which are needed to be able to fully manage all resources in the resolved cluster extension manifest. | | MaxItems: 64
| + + +#### ClusterExtensionRulesStatusItem + + + + + + + +_Appears in:_ +- [ClusterExtensionRulesStatus](#clusterextensionrulesstatus) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `namespace` _string_ | namespace is a reference to a Kubernetes namespace.
This is the namespace where the ClusterExtension's service account is lacking
the rules provided in missingRules.

When namespace is non-empty, one or more RoleBindings should be created to reference
Roles and/or ClusterRoles that provided the missingRules in the namespace.

When namespace is the empty string, one or more ClusterRoleBindings should be created
to reference ClusterRoles that provide the missingRules cluster-wide.

namespace is required and follows the DNS label standard
as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters or hyphens (-),
start and end with an alphanumeric character, and be no longer than 63 characters

[RFC 1123]: https://tools.ietf.org/html/rfc1123 | | MaxLength: 63
Required: \{\}
| +| `rules` _[PolicyRule](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#policyrule-v1-rbac) array_ | rules is a list of RBAC policy rules that are related to the management of the rendered manifest of the
ClusterExtension's bundle.

rules is optional. If it is empty or nil, the semantic meaning is that there are no rules in this grouping
for the named namespace. | | MaxItems: 1024
| + + #### ClusterExtensionSpec @@ -326,6 +359,7 @@ _Appears in:_ | --- | --- | --- | --- | | `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | The set of condition types which apply to all spec.source variations are Installed and Progressing.

The Installed condition represents whether or not the bundle has been installed for this ClusterExtension.
When Installed is True and the Reason is Succeeded, the bundle has been successfully installed.
When Installed is False and the Reason is Failed, the bundle has failed to install.

The Progressing condition represents whether or not the ClusterExtension is advancing towards a new state.
When Progressing is True and the Reason is Succeeded, the ClusterExtension is making progress towards a new state.
When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts.
When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery.

When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition.
These are indications from a package owner to guide users away from a particular package, channel, or bundle.
BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog.
ChannelDeprecated is set if the requested channel is marked deprecated in the catalog.
PackageDeprecated is set if the requested package is marked deprecated in the catalog.
Deprecated is a rollup condition that is present when any of the deprecated conditions are present. | | | | `install` _[ClusterExtensionInstallStatus](#clusterextensioninstallstatus)_ | install is a representation of the current installation status for this ClusterExtension. | | | +| `rules` _[ClusterExtensionRulesStatus](#clusterextensionrulesstatus)_ | rules is a representation of an assessment of permissions related
to the cluster extension and its service account. | | | #### ImageSource diff --git a/go.mod b/go.mod index f6316e0ed..1ddaff530 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( k8s.io/cli-runtime v0.32.1 k8s.io/client-go v0.32.2 k8s.io/component-base v0.32.2 + k8s.io/component-helpers v0.32.1 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20241210054802-24370beab758 sigs.k8s.io/controller-runtime v0.20.2 diff --git a/go.sum b/go.sum index 5cdcac614..d5d6af200 100644 --- a/go.sum +++ b/go.sum @@ -1007,6 +1007,8 @@ k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= k8s.io/component-base v0.32.2 h1:1aUL5Vdmu7qNo4ZsE+569PV5zFatM9hl+lb3dEea2zU= k8s.io/component-base v0.32.2/go.mod h1:PXJ61Vx9Lg+P5mS8TLd7bCIr+eMJRQTyXe8KvkrvJq0= +k8s.io/component-helpers v0.32.1 h1:TwdsSM1vW9GjnfX18lkrZbwE5G9psCIS2/rhenTDXd8= +k8s.io/component-helpers v0.32.1/go.mod h1:1JT1Ei3FD29yFQ18F3laj1WyvxYdHIhyxx6adKMFQXI= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg= diff --git a/internal/operator-controller/applier/helm.go b/internal/operator-controller/applier/helm.go index 76df085cb..6bd5cd7f7 100644 --- a/internal/operator-controller/applier/helm.go +++ b/internal/operator-controller/applier/helm.go @@ -2,11 +2,13 @@ package applier import ( "bytes" + "cmp" "context" "errors" "fmt" "io" "io/fs" + "slices" "strings" "helm.sh/helm/v3/pkg/action" @@ -18,12 +20,14 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" apimachyaml "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/apiserver/pkg/authentication/user" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client" ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/authorization" "github.com/operator-framework/operator-controller/internal/operator-controller/features" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety" @@ -56,6 +60,7 @@ type Preflight interface { type Helm struct { ActionClientGetter helmclient.ActionClientGetter Preflights []Preflight + PreAuthorizer authorization.PreAuthorizer } // shouldSkipPreflight is a helper to determine if the preflight check is CRDUpgradeSafety AND @@ -85,18 +90,60 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte } values := chartutil.Values{} + post := &postrenderer{ + labels: objectLabels, + } + + if features.OperatorControllerFeatureGate.Enabled(features.PreflightPermissions) { + tmplRel, err := h.template(ctx, ext, chrt, values, post) + if err != nil { + return nil, "", fmt.Errorf("failed to get release state using client-only dry-run: %w", err) + } + + ceServiceAccount := user.DefaultInfo{Name: fmt.Sprintf("system:serviceaccount:%s:%s", ext.Spec.Namespace, ext.Spec.ServiceAccount.Name)} + missingRules, err := h.PreAuthorizer.PreAuthorize(ctx, &ceServiceAccount, strings.NewReader(tmplRel.Manifest)) + + var preAuthErrors []error + ext.Status.Rules = nil + if len(missingRules) > 0 { + var missingRulesItems []ocv1.ClusterExtensionRulesStatusItem + for ns, policyRules := range missingRules { + item := ocv1.ClusterExtensionRulesStatusItem{ + Namespace: ns, + Rules: policyRules, + } + if len(item.Rules) > 1024 { + item.Rules = item.Rules[:1024] + } + missingRulesItems = append(missingRulesItems, item) + } + slices.SortFunc(missingRulesItems, func(a, b ocv1.ClusterExtensionRulesStatusItem) int { + return cmp.Compare(a.Namespace, b.Namespace) + }) + + if len(missingRulesItems) > 64 { + missingRulesItems = missingRulesItems[:64] + } + + ext.Status.Rules = &ocv1.ClusterExtensionRulesStatus{Missing: missingRulesItems} + preAuthErrors = append(preAuthErrors, fmt.Errorf("service account lacks permission to manage cluster extension")) + } + if err != nil { + preAuthErrors = append(preAuthErrors, fmt.Errorf("authorization evaluation error: %w", err)) + } + if len(preAuthErrors) > 0 { + return nil, "", fmt.Errorf("pre-authorization failed: %v", preAuthErrors) + } + } + ac, err := h.ActionClientGetter.ActionClientFor(ctx, ext) if err != nil { return nil, "", err } - post := &postrenderer{ - labels: objectLabels, - } - rel, desiredRel, state, err := h.getReleaseState(ac, ext, chrt, values, post) if err != nil { - return nil, "", err + return nil, "", fmt.Errorf("failed to get release state using server-side dry-run: %w", err) } for _, preflight := range h.Preflights { @@ -152,6 +199,34 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte return relObjects, state, nil } +func (h *Helm) template(ctx context.Context, ext *ocv1.ClusterExtension, chrt *chart.Chart, values chartutil.Values, post postrender.PostRenderer) (*release.Release, error) { + // We need to get a separate action client because our template call below + // permanently modifies the underlying action.Configuration for ClientOnly mode. + ac, err := h.ActionClientGetter.ActionClientFor(ctx, ext) + if err != nil { + return nil, err + } + + isUpgrade := false + currentRelease, err := ac.Get(ext.GetName()) + if err != nil && !errors.Is(err, driver.ErrReleaseNotFound) { + return nil, err + } + if currentRelease != nil { + isUpgrade = true + } + + return ac.Install(ext.GetName(), ext.Spec.Namespace, chrt, values, func(i *action.Install) error { + i.DryRun = true + i.ReleaseName = ext.GetName() + i.Replace = true + i.ClientOnly = true + i.IncludeCRDs = true + i.IsUpgrade = isUpgrade + return nil + }, helmclient.AppendInstallPostRenderer(post)) +} + func (h *Helm) getReleaseState(cl helmclient.ActionInterface, ext *ocv1.ClusterExtension, chrt *chart.Chart, values chartutil.Values, post postrender.PostRenderer) (*release.Release, *release.Release, string, error) { currentRelease, err := cl.Get(ext.GetName()) if errors.Is(err, driver.ErrReleaseNotFound) { @@ -161,10 +236,6 @@ func (h *Helm) getReleaseState(cl helmclient.ActionInterface, ext *ocv1.ClusterE return nil }, helmclient.AppendInstallPostRenderer(post)) if err != nil { - if features.OperatorControllerFeatureGate.Enabled(features.PreflightPermissions) { - _ = struct{}{} // minimal no-op to satisfy linter - // probably need to break out this error as it's the one for helm dry-run as opposed to any returned later - } return nil, nil, StateError, err } return nil, desiredRelease, StateNeedsInstall, nil diff --git a/internal/operator-controller/applier/helm_test.go b/internal/operator-controller/applier/helm_test.go index b170d8a98..9c150cbcd 100644 --- a/internal/operator-controller/applier/helm_test.go +++ b/internal/operator-controller/applier/helm_test.go @@ -13,14 +13,12 @@ import ( "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" - featuregatetesting "k8s.io/component-base/featuregate/testing" "sigs.k8s.io/controller-runtime/pkg/client" helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client" ocv1 "github.com/operator-framework/operator-controller/api/v1" "github.com/operator-framework/operator-controller/internal/operator-controller/applier" - "github.com/operator-framework/operator-controller/internal/operator-controller/features" ) type mockPreflight struct { @@ -228,71 +226,6 @@ func TestApply_Installation(t *testing.T) { }) } -func TestApply_InstallationWithPreflightPermissionsEnabled(t *testing.T) { - featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.PreflightPermissions, true) - - t.Run("fails during dry-run installation", func(t *testing.T) { - mockAcg := &mockActionGetter{ - getClientErr: driver.ErrReleaseNotFound, - dryRunInstallErr: errors.New("failed attempting to dry-run install chart"), - } - helmApplier := applier.Helm{ActionClientGetter: mockAcg} - - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) - require.Error(t, err) - require.ErrorContains(t, err, "attempting to dry-run install chart") - require.Nil(t, objs) - require.Empty(t, state) - }) - - t.Run("fails during pre-flight installation", func(t *testing.T) { - mockAcg := &mockActionGetter{ - getClientErr: driver.ErrReleaseNotFound, - installErr: errors.New("failed installing chart"), - } - mockPf := &mockPreflight{installErr: errors.New("failed during install pre-flight check")} - helmApplier := applier.Helm{ActionClientGetter: mockAcg, Preflights: []applier.Preflight{mockPf}} - - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) - require.Error(t, err) - require.ErrorContains(t, err, "install pre-flight check") - require.Equal(t, applier.StateNeedsInstall, state) - require.Nil(t, objs) - }) - - t.Run("fails during installation", func(t *testing.T) { - mockAcg := &mockActionGetter{ - getClientErr: driver.ErrReleaseNotFound, - installErr: errors.New("failed installing chart"), - } - helmApplier := applier.Helm{ActionClientGetter: mockAcg} - - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) - require.Error(t, err) - require.ErrorContains(t, err, "installing chart") - require.Equal(t, applier.StateNeedsInstall, state) - require.Nil(t, objs) - }) - - t.Run("successful installation", func(t *testing.T) { - mockAcg := &mockActionGetter{ - getClientErr: driver.ErrReleaseNotFound, - desiredRel: &release.Release{ - Info: &release.Info{Status: release.StatusDeployed}, - Manifest: validManifest, - }, - } - helmApplier := applier.Helm{ActionClientGetter: mockAcg} - - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) - require.NoError(t, err) - require.Equal(t, applier.StateNeedsInstall, state) - require.NotNil(t, objs) - assert.Equal(t, "service-a", objs[0].GetName()) - assert.Equal(t, "service-b", objs[1].GetName()) - }) -} - func TestApply_Upgrade(t *testing.T) { testCurrentRelease := &release.Release{ Info: &release.Info{Status: release.StatusDeployed}, diff --git a/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/helpers.go b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/helpers.go new file mode 100644 index 000000000..00aa4cae1 --- /dev/null +++ b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/helpers.go @@ -0,0 +1,375 @@ +/* +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 rbac + +import ( + "fmt" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" +) + +// ResourceMatches returns the result of the rule.Resources matching. +func ResourceMatches(rule *PolicyRule, combinedRequestedResource, requestedSubresource string) bool { + for _, ruleResource := range rule.Resources { + // if everything is allowed, we match + if ruleResource == ResourceAll { + return true + } + // if we have an exact match, we match + if ruleResource == combinedRequestedResource { + return true + } + + // We can also match a */subresource. + // if there isn't a subresource, then continue + if len(requestedSubresource) == 0 { + continue + } + // if the rule isn't in the format */subresource, then we don't match, continue + if len(ruleResource) == len(requestedSubresource)+2 && + strings.HasPrefix(ruleResource, "*/") && + strings.HasSuffix(ruleResource, requestedSubresource) { + return true + + } + } + + return false +} + +// SubjectsStrings returns users, groups, serviceaccounts, unknown for display purposes. +func SubjectsStrings(subjects []Subject) ([]string, []string, []string, []string) { + users := []string{} + groups := []string{} + sas := []string{} + others := []string{} + + for _, subject := range subjects { + switch subject.Kind { + case ServiceAccountKind: + sas = append(sas, fmt.Sprintf("%s/%s", subject.Namespace, subject.Name)) + + case UserKind: + users = append(users, subject.Name) + + case GroupKind: + groups = append(groups, subject.Name) + + default: + others = append(others, fmt.Sprintf("%s/%s/%s", subject.Kind, subject.Namespace, subject.Name)) + } + } + + return users, groups, sas, others +} + +func (r PolicyRule) String() string { + return "PolicyRule" + r.CompactString() +} + +// CompactString exposes a compact string representation for use in escalation error messages +func (r PolicyRule) CompactString() string { + formatStringParts := []string{} + formatArgs := []interface{}{} + if len(r.APIGroups) > 0 { + formatStringParts = append(formatStringParts, "APIGroups:%q") + formatArgs = append(formatArgs, r.APIGroups) + } + if len(r.Resources) > 0 { + formatStringParts = append(formatStringParts, "Resources:%q") + formatArgs = append(formatArgs, r.Resources) + } + if len(r.NonResourceURLs) > 0 { + formatStringParts = append(formatStringParts, "NonResourceURLs:%q") + formatArgs = append(formatArgs, r.NonResourceURLs) + } + if len(r.ResourceNames) > 0 { + formatStringParts = append(formatStringParts, "ResourceNames:%q") + formatArgs = append(formatArgs, r.ResourceNames) + } + if len(r.Verbs) > 0 { + formatStringParts = append(formatStringParts, "Verbs:%q") + formatArgs = append(formatArgs, r.Verbs) + } + formatString := "{" + strings.Join(formatStringParts, ", ") + "}" + return fmt.Sprintf(formatString, formatArgs...) +} + +// PolicyRuleBuilder let's us attach methods. A no-no for API types. +// We use it to construct rules in code. It's more compact than trying to write them +// out in a literal and allows us to perform some basic checking during construction +// +k8s:deepcopy-gen=false +type PolicyRuleBuilder struct { + PolicyRule PolicyRule +} + +// NewRule returns new PolicyRule made by input verbs. +func NewRule(verbs ...string) *PolicyRuleBuilder { + return &PolicyRuleBuilder{ + PolicyRule: PolicyRule{Verbs: sets.NewString(verbs...).List()}, + } +} + +// Groups combines the PolicyRule.APIGroups and input groups. +func (r *PolicyRuleBuilder) Groups(groups ...string) *PolicyRuleBuilder { + r.PolicyRule.APIGroups = combine(r.PolicyRule.APIGroups, groups) + return r +} + +// Resources combines the PolicyRule.Rule and input resources. +func (r *PolicyRuleBuilder) Resources(resources ...string) *PolicyRuleBuilder { + r.PolicyRule.Resources = combine(r.PolicyRule.Resources, resources) + return r +} + +// Names combines the PolicyRule.ResourceNames and input names. +func (r *PolicyRuleBuilder) Names(names ...string) *PolicyRuleBuilder { + r.PolicyRule.ResourceNames = combine(r.PolicyRule.ResourceNames, names) + return r +} + +// URLs combines the PolicyRule.NonResourceURLs and input urls. +func (r *PolicyRuleBuilder) URLs(urls ...string) *PolicyRuleBuilder { + r.PolicyRule.NonResourceURLs = combine(r.PolicyRule.NonResourceURLs, urls) + return r +} + +// RuleOrDie calls the binding method and panics if there is an error. +func (r *PolicyRuleBuilder) RuleOrDie() PolicyRule { + ret, err := r.Rule() + if err != nil { + panic(err) + } + return ret +} + +func combine(s1, s2 []string) []string { + s := sets.NewString(s1...) + s.Insert(s2...) + return s.List() +} + +// Rule returns PolicyRule and error. +func (r *PolicyRuleBuilder) Rule() (PolicyRule, error) { + if len(r.PolicyRule.Verbs) == 0 { + return PolicyRule{}, fmt.Errorf("verbs are required: %#v", r.PolicyRule) + } + + switch { + case len(r.PolicyRule.NonResourceURLs) > 0: + if len(r.PolicyRule.APIGroups) != 0 || len(r.PolicyRule.Resources) != 0 || len(r.PolicyRule.ResourceNames) != 0 { + return PolicyRule{}, fmt.Errorf("non-resource rule may not have apiGroups, resources, or resourceNames: %#v", r.PolicyRule) + } + case len(r.PolicyRule.Resources) > 0: + // resource rule may not have nonResourceURLs + + if len(r.PolicyRule.APIGroups) == 0 { + // this a common bug + return PolicyRule{}, fmt.Errorf("resource rule must have apiGroups: %#v", r.PolicyRule) + } + // if resource names are set, then the verb must not be list, watch, create, or deletecollection + // since verbs are largely opaque, we don't want to accidentally prevent things like "impersonate", so + // we will backlist common mistakes, not whitelist acceptable options. + if len(r.PolicyRule.ResourceNames) != 0 { + illegalVerbs := []string{} + for _, verb := range r.PolicyRule.Verbs { + switch verb { + case "list", "watch", "create", "deletecollection": + illegalVerbs = append(illegalVerbs, verb) + } + } + if len(illegalVerbs) > 0 { + return PolicyRule{}, fmt.Errorf("verbs %v do not have names available: %#v", illegalVerbs, r.PolicyRule) + } + } + + default: + return PolicyRule{}, fmt.Errorf("a rule must have either nonResourceURLs or resources: %#v", r.PolicyRule) + } + + return r.PolicyRule, nil +} + +// ClusterRoleBindingBuilder let's us attach methods. A no-no for API types. +// We use it to construct bindings in code. It's more compact than trying to write them +// out in a literal. +// +k8s:deepcopy-gen=false +type ClusterRoleBindingBuilder struct { + ClusterRoleBinding ClusterRoleBinding +} + +// NewClusterBinding creates a ClusterRoleBinding builder that can be used +// to define the subjects of a cluster role binding. At least one of +// the `Groups`, `Users` or `SAs` method must be called before +// calling the `Binding*` methods. +func NewClusterBinding(clusterRoleName string) *ClusterRoleBindingBuilder { + return &ClusterRoleBindingBuilder{ + ClusterRoleBinding: ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: clusterRoleName}, + RoleRef: RoleRef{ + APIGroup: GroupName, + Kind: "ClusterRole", + Name: clusterRoleName, + }, + }, + } +} + +// Groups adds the specified groups as the subjects of the ClusterRoleBinding. +func (r *ClusterRoleBindingBuilder) Groups(groups ...string) *ClusterRoleBindingBuilder { + for _, group := range groups { + r.ClusterRoleBinding.Subjects = append(r.ClusterRoleBinding.Subjects, Subject{Kind: GroupKind, APIGroup: GroupName, Name: group}) + } + return r +} + +// Users adds the specified users as the subjects of the ClusterRoleBinding. +func (r *ClusterRoleBindingBuilder) Users(users ...string) *ClusterRoleBindingBuilder { + for _, user := range users { + r.ClusterRoleBinding.Subjects = append(r.ClusterRoleBinding.Subjects, Subject{Kind: UserKind, APIGroup: GroupName, Name: user}) + } + return r +} + +// SAs adds the specified sas as the subjects of the ClusterRoleBinding. +func (r *ClusterRoleBindingBuilder) SAs(namespace string, serviceAccountNames ...string) *ClusterRoleBindingBuilder { + for _, saName := range serviceAccountNames { + r.ClusterRoleBinding.Subjects = append(r.ClusterRoleBinding.Subjects, Subject{Kind: ServiceAccountKind, Namespace: namespace, Name: saName}) + } + return r +} + +// BindingOrDie calls the binding method and panics if there is an error. +func (r *ClusterRoleBindingBuilder) BindingOrDie() ClusterRoleBinding { + ret, err := r.Binding() + if err != nil { + panic(err) + } + return ret +} + +// Binding builds and returns the ClusterRoleBinding API object from the builder +// object. +func (r *ClusterRoleBindingBuilder) Binding() (ClusterRoleBinding, error) { + if len(r.ClusterRoleBinding.Subjects) == 0 { + return ClusterRoleBinding{}, fmt.Errorf("subjects are required: %#v", r.ClusterRoleBinding) + } + + return r.ClusterRoleBinding, nil +} + +// RoleBindingBuilder let's us attach methods. It is similar to +// ClusterRoleBindingBuilder above. +// +k8s:deepcopy-gen=false +type RoleBindingBuilder struct { + RoleBinding RoleBinding +} + +// NewRoleBinding creates a RoleBinding builder that can be used +// to define the subjects of a role binding. At least one of +// the `Groups`, `Users` or `SAs` method must be called before +// calling the `Binding*` methods. +func NewRoleBinding(roleName, namespace string) *RoleBindingBuilder { + return &RoleBindingBuilder{ + RoleBinding: RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: roleName, + Namespace: namespace, + }, + RoleRef: RoleRef{ + APIGroup: GroupName, + Kind: "Role", + Name: roleName, + }, + }, + } +} + +// NewRoleBindingForClusterRole creates a RoleBinding builder that can be used +// to define the subjects of a cluster role binding. At least one of +// the `Groups`, `Users` or `SAs` method must be called before +// calling the `Binding*` methods. +func NewRoleBindingForClusterRole(roleName, namespace string) *RoleBindingBuilder { + return &RoleBindingBuilder{ + RoleBinding: RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: roleName, + Namespace: namespace, + }, + RoleRef: RoleRef{ + APIGroup: GroupName, + Kind: "ClusterRole", + Name: roleName, + }, + }, + } +} + +// Groups adds the specified groups as the subjects of the RoleBinding. +func (r *RoleBindingBuilder) Groups(groups ...string) *RoleBindingBuilder { + for _, group := range groups { + r.RoleBinding.Subjects = append(r.RoleBinding.Subjects, Subject{Kind: GroupKind, APIGroup: GroupName, Name: group}) + } + return r +} + +// Users adds the specified users as the subjects of the RoleBinding. +func (r *RoleBindingBuilder) Users(users ...string) *RoleBindingBuilder { + for _, user := range users { + r.RoleBinding.Subjects = append(r.RoleBinding.Subjects, Subject{Kind: UserKind, APIGroup: GroupName, Name: user}) + } + return r +} + +// SAs adds the specified service accounts as the subjects of the +// RoleBinding. +func (r *RoleBindingBuilder) SAs(namespace string, serviceAccountNames ...string) *RoleBindingBuilder { + for _, saName := range serviceAccountNames { + r.RoleBinding.Subjects = append(r.RoleBinding.Subjects, Subject{Kind: ServiceAccountKind, Namespace: namespace, Name: saName}) + } + return r +} + +// BindingOrDie calls the binding method and panics if there is an error. +func (r *RoleBindingBuilder) BindingOrDie() RoleBinding { + ret, err := r.Binding() + if err != nil { + panic(err) + } + return ret +} + +// Binding builds and returns the RoleBinding API object from the builder +// object. +func (r *RoleBindingBuilder) Binding() (RoleBinding, error) { + if len(r.RoleBinding.Subjects) == 0 { + return RoleBinding{}, fmt.Errorf("subjects are required: %#v", r.RoleBinding) + } + + return r.RoleBinding, nil +} + +// SortableRuleSlice is the slice of PolicyRule. +type SortableRuleSlice []PolicyRule + +func (s SortableRuleSlice) Len() int { return len(s) } +func (s SortableRuleSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s SortableRuleSlice) Less(i, j int) bool { + return strings.Compare(s[i].String(), s[j].String()) < 0 +} diff --git a/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/register.go b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/register.go new file mode 100644 index 000000000..48f5f6e74 --- /dev/null +++ b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/register.go @@ -0,0 +1,60 @@ +/* +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 rbac + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GroupName is the name of this API group. +const GroupName = "rbac.authorization.k8s.io" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal} + +// Kind takes an unqualified kind and returns a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +// SchemeBuilder is a function that calls Register for you. +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &Role{}, + &RoleBinding{}, + &RoleBindingList{}, + &RoleList{}, + + &ClusterRole{}, + &ClusterRoleBinding{}, + &ClusterRoleBindingList{}, + &ClusterRoleList{}, + ) + return nil +} diff --git a/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/types.go b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/types.go new file mode 100644 index 000000000..357e8d8bc --- /dev/null +++ b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/types.go @@ -0,0 +1,210 @@ +/* +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 rbac + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Authorization is calculated against +// 1. evaluation of ClusterRoleBindings - short circuit on match +// 2. evaluation of RoleBindings in the namespace requested - short circuit on match +// 3. deny by default + +// APIGroupAll and these consts are default values for rbac authorization. +const ( + APIGroupAll = "*" + ResourceAll = "*" + VerbAll = "*" + NonResourceAll = "*" + + GroupKind = "Group" + ServiceAccountKind = "ServiceAccount" + UserKind = "User" + + // AutoUpdateAnnotationKey is the name of an annotation which prevents reconciliation if set to "false" + AutoUpdateAnnotationKey = "rbac.authorization.kubernetes.io/autoupdate" +) + +// PolicyRule holds information that describes a policy rule, but does not contain information +// about who the rule applies to or which namespace the rule applies to. +type PolicyRule struct { + // Verbs is a list of Verbs that apply to ALL the ResourceKinds contained in this rule. '*' represents all verbs. + Verbs []string + + // APIGroups is the name of the APIGroup that contains the resources. + // If multiple API groups are specified, any action requested against one of the enumerated resources in any API group will be allowed. "" represents the core API group and "*" represents all API groups. + APIGroups []string + // Resources is a list of resources this rule applies to. '*' represents all resources in the specified apiGroups. + // '*/foo' represents the subresource 'foo' for all resources in the specified apiGroups. + Resources []string + // ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. + ResourceNames []string + + // NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path + // If an action is not a resource API request, then the URL is split on '/' and is checked against the NonResourceURLs to look for a match. + // Since non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding. + // Rules can either apply to API resources (such as "pods" or "secrets") or non-resource URL paths (such as "/api"), but not both. + NonResourceURLs []string +} + +// Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, +// or a value for non-objects such as user and group names. +type Subject struct { + // Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". + // If the Authorizer does not recognized the kind value, the Authorizer should report an error. + Kind string + // APIGroup holds the API group of the referenced subject. + // Defaults to "" for ServiceAccount subjects. + // Defaults to "rbac.authorization.k8s.io" for User and Group subjects. + APIGroup string + // Name of the object being referenced. + Name string + // Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty + // the Authorizer should report an error. + Namespace string +} + +// RoleRef contains information that points to the role being used +type RoleRef struct { + // APIGroup is the group for the resource being referenced + APIGroup string + // Kind is the type of resource being referenced + Kind string + // Name is the name of resource being referenced + Name string +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Role is a namespaced, logical grouping of PolicyRules that can be referenced as a unit by a RoleBinding. +type Role struct { + metav1.TypeMeta + // Standard object's metadata. + metav1.ObjectMeta + + // Rules holds all the PolicyRules for this Role + Rules []PolicyRule +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// RoleBinding references a role, but does not contain it. It can reference a Role in the same namespace or a ClusterRole in the global namespace. +// It adds who information via Subjects and namespace information by which namespace it exists in. RoleBindings in a given +// namespace only have effect in that namespace. +type RoleBinding struct { + metav1.TypeMeta + metav1.ObjectMeta + + // Subjects holds references to the objects the role applies to. + Subjects []Subject + + // RoleRef can reference a Role in the current namespace or a ClusterRole in the global namespace. + // If the RoleRef cannot be resolved, the Authorizer must return an error. + // This field is immutable. + RoleRef RoleRef +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// RoleBindingList is a collection of RoleBindings +type RoleBindingList struct { + metav1.TypeMeta + // Standard object's metadata. + metav1.ListMeta + + // Items is a list of roleBindings + Items []RoleBinding +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// RoleList is a collection of Roles +type RoleList struct { + metav1.TypeMeta + // Standard object's metadata. + metav1.ListMeta + + // Items is a list of roles + Items []Role +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClusterRole is a cluster level, logical grouping of PolicyRules that can be referenced as a unit by a RoleBinding or ClusterRoleBinding. +type ClusterRole struct { + metav1.TypeMeta + // Standard object's metadata. + metav1.ObjectMeta + + // Rules holds all the PolicyRules for this ClusterRole + Rules []PolicyRule + + // AggregationRule is an optional field that describes how to build the Rules for this ClusterRole. + // If AggregationRule is set, then the Rules are controller managed and direct changes to Rules will be + // stomped by the controller. + AggregationRule *AggregationRule +} + +// AggregationRule describes how to locate ClusterRoles to aggregate into the ClusterRole +type AggregationRule struct { + // ClusterRoleSelectors holds a list of selectors which will be used to find ClusterRoles and create the rules. + // If any of the selectors match, then the ClusterRole's permissions will be added + ClusterRoleSelectors []metav1.LabelSelector +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClusterRoleBinding references a ClusterRole, but not contain it. It can reference a ClusterRole in the global namespace, +// and adds who information via Subject. +type ClusterRoleBinding struct { + metav1.TypeMeta + // Standard object's metadata. + metav1.ObjectMeta + + // Subjects holds references to the objects the role applies to. + Subjects []Subject + + // RoleRef can only reference a ClusterRole in the global namespace. + // If the RoleRef cannot be resolved, the Authorizer must return an error. + // This field is immutable. + RoleRef RoleRef +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClusterRoleBindingList is a collection of ClusterRoleBindings +type ClusterRoleBindingList struct { + metav1.TypeMeta + // Standard object's metadata. + metav1.ListMeta + + // Items is a list of ClusterRoleBindings + Items []ClusterRoleBinding +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClusterRoleList is a collection of ClusterRoles +type ClusterRoleList struct { + metav1.TypeMeta + // Standard object's metadata. + metav1.ListMeta + + // Items is a list of ClusterRoles + Items []ClusterRole +} diff --git a/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/defaults.go b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/defaults.go new file mode 100644 index 000000000..7d285a857 --- /dev/null +++ b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/defaults.go @@ -0,0 +1,49 @@ +/* +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 v1 + +import ( + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func addDefaultingFuncs(scheme *runtime.Scheme) error { + return RegisterDefaults(scheme) +} + +func SetDefaults_ClusterRoleBinding(obj *rbacv1.ClusterRoleBinding) { + if len(obj.RoleRef.APIGroup) == 0 { + obj.RoleRef.APIGroup = GroupName + } +} +func SetDefaults_RoleBinding(obj *rbacv1.RoleBinding) { + if len(obj.RoleRef.APIGroup) == 0 { + obj.RoleRef.APIGroup = GroupName + } +} +func SetDefaults_Subject(obj *rbacv1.Subject) { + if len(obj.APIGroup) == 0 { + switch obj.Kind { + case rbacv1.ServiceAccountKind: + obj.APIGroup = "" + case rbacv1.UserKind: + obj.APIGroup = GroupName + case rbacv1.GroupKind: + obj.APIGroup = GroupName + } + } +} diff --git a/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/evaluation_helpers.go b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/evaluation_helpers.go new file mode 100644 index 000000000..5f5edaff1 --- /dev/null +++ b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/evaluation_helpers.go @@ -0,0 +1,144 @@ +/* +Copyright 2018 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 v1 + +import ( + "fmt" + "strings" + + rbacv1 "k8s.io/api/rbac/v1" +) + +func VerbMatches(rule *rbacv1.PolicyRule, requestedVerb string) bool { + for _, ruleVerb := range rule.Verbs { + if ruleVerb == rbacv1.VerbAll { + return true + } + if ruleVerb == requestedVerb { + return true + } + } + + return false +} + +func APIGroupMatches(rule *rbacv1.PolicyRule, requestedGroup string) bool { + for _, ruleGroup := range rule.APIGroups { + if ruleGroup == rbacv1.APIGroupAll { + return true + } + if ruleGroup == requestedGroup { + return true + } + } + + return false +} + +func ResourceMatches(rule *rbacv1.PolicyRule, combinedRequestedResource, requestedSubresource string) bool { + for _, ruleResource := range rule.Resources { + // if everything is allowed, we match + if ruleResource == rbacv1.ResourceAll { + return true + } + // if we have an exact match, we match + if ruleResource == combinedRequestedResource { + return true + } + + // We can also match a */subresource. + // if there isn't a subresource, then continue + if len(requestedSubresource) == 0 { + continue + } + // if the rule isn't in the format */subresource, then we don't match, continue + if len(ruleResource) == len(requestedSubresource)+2 && + strings.HasPrefix(ruleResource, "*/") && + strings.HasSuffix(ruleResource, requestedSubresource) { + return true + + } + } + + return false +} + +func ResourceNameMatches(rule *rbacv1.PolicyRule, requestedName string) bool { + if len(rule.ResourceNames) == 0 { + return true + } + + for _, ruleName := range rule.ResourceNames { + if ruleName == requestedName { + return true + } + } + + return false +} + +func NonResourceURLMatches(rule *rbacv1.PolicyRule, requestedURL string) bool { + for _, ruleURL := range rule.NonResourceURLs { + if ruleURL == rbacv1.NonResourceAll { + return true + } + if ruleURL == requestedURL { + return true + } + if strings.HasSuffix(ruleURL, "*") && strings.HasPrefix(requestedURL, strings.TrimRight(ruleURL, "*")) { + return true + } + } + + return false +} + +// CompactString exposes a compact string representation for use in escalation error messages +func CompactString(r rbacv1.PolicyRule) string { + formatStringParts := []string{} + formatArgs := []interface{}{} + if len(r.APIGroups) > 0 { + formatStringParts = append(formatStringParts, "APIGroups:%q") + formatArgs = append(formatArgs, r.APIGroups) + } + if len(r.Resources) > 0 { + formatStringParts = append(formatStringParts, "Resources:%q") + formatArgs = append(formatArgs, r.Resources) + } + if len(r.NonResourceURLs) > 0 { + formatStringParts = append(formatStringParts, "NonResourceURLs:%q") + formatArgs = append(formatArgs, r.NonResourceURLs) + } + if len(r.ResourceNames) > 0 { + formatStringParts = append(formatStringParts, "ResourceNames:%q") + formatArgs = append(formatArgs, r.ResourceNames) + } + if len(r.Verbs) > 0 { + formatStringParts = append(formatStringParts, "Verbs:%q") + formatArgs = append(formatArgs, r.Verbs) + } + formatString := "{" + strings.Join(formatStringParts, ", ") + "}" + return fmt.Sprintf(formatString, formatArgs...) +} + +type SortableRuleSlice []rbacv1.PolicyRule + +func (s SortableRuleSlice) Len() int { return len(s) } +func (s SortableRuleSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s SortableRuleSlice) Less(i, j int) bool { + return strings.Compare(s[i].String(), s[j].String()) < 0 +} diff --git a/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/helpers.go b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/helpers.go new file mode 100644 index 000000000..669e48c8a --- /dev/null +++ b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/helpers.go @@ -0,0 +1,231 @@ +/* +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 v1 + +import ( + "fmt" + + rbacv1 "k8s.io/api/rbac/v1" + + "sort" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen=false + +// PolicyRuleBuilder let's us attach methods. A no-no for API types. +// We use it to construct rules in code. It's more compact than trying to write them +// out in a literal and allows us to perform some basic checking during construction +type PolicyRuleBuilder struct { + PolicyRule rbacv1.PolicyRule `protobuf:"bytes,1,opt,name=policyRule"` +} + +func NewRule(verbs ...string) *PolicyRuleBuilder { + return &PolicyRuleBuilder{ + PolicyRule: rbacv1.PolicyRule{Verbs: verbs}, + } +} + +func (r *PolicyRuleBuilder) Groups(groups ...string) *PolicyRuleBuilder { + r.PolicyRule.APIGroups = append(r.PolicyRule.APIGroups, groups...) + return r +} + +func (r *PolicyRuleBuilder) Resources(resources ...string) *PolicyRuleBuilder { + r.PolicyRule.Resources = append(r.PolicyRule.Resources, resources...) + return r +} + +func (r *PolicyRuleBuilder) Names(names ...string) *PolicyRuleBuilder { + r.PolicyRule.ResourceNames = append(r.PolicyRule.ResourceNames, names...) + return r +} + +func (r *PolicyRuleBuilder) URLs(urls ...string) *PolicyRuleBuilder { + r.PolicyRule.NonResourceURLs = append(r.PolicyRule.NonResourceURLs, urls...) + return r +} + +func (r *PolicyRuleBuilder) RuleOrDie() rbacv1.PolicyRule { + ret, err := r.Rule() + if err != nil { + panic(err) + } + return ret +} + +func (r *PolicyRuleBuilder) Rule() (rbacv1.PolicyRule, error) { + if len(r.PolicyRule.Verbs) == 0 { + return rbacv1.PolicyRule{}, fmt.Errorf("verbs are required: %#v", r.PolicyRule) + } + + switch { + case len(r.PolicyRule.NonResourceURLs) > 0: + if len(r.PolicyRule.APIGroups) != 0 || len(r.PolicyRule.Resources) != 0 || len(r.PolicyRule.ResourceNames) != 0 { + return rbacv1.PolicyRule{}, fmt.Errorf("non-resource rule may not have apiGroups, resources, or resourceNames: %#v", r.PolicyRule) + } + case len(r.PolicyRule.Resources) > 0: + if len(r.PolicyRule.NonResourceURLs) != 0 { + return rbacv1.PolicyRule{}, fmt.Errorf("resource rule may not have nonResourceURLs: %#v", r.PolicyRule) + } + if len(r.PolicyRule.APIGroups) == 0 { + // this a common bug + return rbacv1.PolicyRule{}, fmt.Errorf("resource rule must have apiGroups: %#v", r.PolicyRule) + } + default: + return rbacv1.PolicyRule{}, fmt.Errorf("a rule must have either nonResourceURLs or resources: %#v", r.PolicyRule) + } + + sort.Strings(r.PolicyRule.Resources) + sort.Strings(r.PolicyRule.ResourceNames) + sort.Strings(r.PolicyRule.APIGroups) + sort.Strings(r.PolicyRule.NonResourceURLs) + sort.Strings(r.PolicyRule.Verbs) + return r.PolicyRule, nil +} + +// +k8s:deepcopy-gen=false + +// ClusterRoleBindingBuilder let's us attach methods. A no-no for API types. +// We use it to construct bindings in code. It's more compact than trying to write them +// out in a literal. +type ClusterRoleBindingBuilder struct { + ClusterRoleBinding rbacv1.ClusterRoleBinding `protobuf:"bytes,1,opt,name=clusterRoleBinding"` +} + +func NewClusterBinding(clusterRoleName string) *ClusterRoleBindingBuilder { + return &ClusterRoleBindingBuilder{ + ClusterRoleBinding: rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: clusterRoleName}, + RoleRef: rbacv1.RoleRef{ + APIGroup: GroupName, + Kind: "ClusterRole", + Name: clusterRoleName, + }, + }, + } +} + +func (r *ClusterRoleBindingBuilder) Groups(groups ...string) *ClusterRoleBindingBuilder { + for _, group := range groups { + r.ClusterRoleBinding.Subjects = append(r.ClusterRoleBinding.Subjects, rbacv1.Subject{APIGroup: rbacv1.GroupName, Kind: rbacv1.GroupKind, Name: group}) + } + return r +} + +func (r *ClusterRoleBindingBuilder) Users(users ...string) *ClusterRoleBindingBuilder { + for _, user := range users { + r.ClusterRoleBinding.Subjects = append(r.ClusterRoleBinding.Subjects, rbacv1.Subject{APIGroup: rbacv1.GroupName, Kind: rbacv1.UserKind, Name: user}) + } + return r +} + +func (r *ClusterRoleBindingBuilder) SAs(namespace string, serviceAccountNames ...string) *ClusterRoleBindingBuilder { + for _, saName := range serviceAccountNames { + r.ClusterRoleBinding.Subjects = append(r.ClusterRoleBinding.Subjects, rbacv1.Subject{Kind: rbacv1.ServiceAccountKind, Namespace: namespace, Name: saName}) + } + return r +} + +func (r *ClusterRoleBindingBuilder) BindingOrDie() rbacv1.ClusterRoleBinding { + ret, err := r.Binding() + if err != nil { + panic(err) + } + return ret +} + +func (r *ClusterRoleBindingBuilder) Binding() (rbacv1.ClusterRoleBinding, error) { + if len(r.ClusterRoleBinding.Subjects) == 0 { + return rbacv1.ClusterRoleBinding{}, fmt.Errorf("subjects are required: %#v", r.ClusterRoleBinding) + } + + return r.ClusterRoleBinding, nil +} + +// +k8s:deepcopy-gen=false + +// RoleBindingBuilder let's us attach methods. It is similar to +// ClusterRoleBindingBuilder above. +type RoleBindingBuilder struct { + RoleBinding rbacv1.RoleBinding +} + +// NewRoleBinding creates a RoleBinding builder that can be used +// to define the subjects of a role binding. At least one of +// the `Groups`, `Users` or `SAs` method must be called before +// calling the `Binding*` methods. +func NewRoleBinding(roleName, namespace string) *RoleBindingBuilder { + return &RoleBindingBuilder{ + RoleBinding: rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: roleName, + Namespace: namespace, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: GroupName, + Kind: "Role", + Name: roleName, + }, + }, + } +} + +// Groups adds the specified groups as the subjects of the RoleBinding. +func (r *RoleBindingBuilder) Groups(groups ...string) *RoleBindingBuilder { + for _, group := range groups { + r.RoleBinding.Subjects = append(r.RoleBinding.Subjects, rbacv1.Subject{Kind: rbacv1.GroupKind, APIGroup: GroupName, Name: group}) + } + return r +} + +// Users adds the specified users as the subjects of the RoleBinding. +func (r *RoleBindingBuilder) Users(users ...string) *RoleBindingBuilder { + for _, user := range users { + r.RoleBinding.Subjects = append(r.RoleBinding.Subjects, rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: GroupName, Name: user}) + } + return r +} + +// SAs adds the specified service accounts as the subjects of the +// RoleBinding. +func (r *RoleBindingBuilder) SAs(namespace string, serviceAccountNames ...string) *RoleBindingBuilder { + for _, saName := range serviceAccountNames { + r.RoleBinding.Subjects = append(r.RoleBinding.Subjects, rbacv1.Subject{Kind: rbacv1.ServiceAccountKind, Namespace: namespace, Name: saName}) + } + return r +} + +// BindingOrDie calls the binding method and panics if there is an error. +func (r *RoleBindingBuilder) BindingOrDie() rbacv1.RoleBinding { + ret, err := r.Binding() + if err != nil { + panic(err) + } + return ret +} + +// Binding builds and returns the RoleBinding API object from the builder +// object. +func (r *RoleBindingBuilder) Binding() (rbacv1.RoleBinding, error) { + if len(r.RoleBinding.Subjects) == 0 { + return rbacv1.RoleBinding{}, fmt.Errorf("subjects are required: %#v", r.RoleBinding) + } + + return r.RoleBinding, nil +} diff --git a/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/register.go b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/register.go new file mode 100644 index 000000000..ae138c888 --- /dev/null +++ b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/register.go @@ -0,0 +1,44 @@ +/* +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 v1 + +import ( + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const GroupName = "rbac.authorization.k8s.io" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + localSchemeBuilder = &rbacv1.SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) + +func init() { + // We only register manually written functions here. The registration of the + // generated functions takes place in the generated files. The separation + // makes the code compile even when the generated files are missing. + localSchemeBuilder.Register(addDefaultingFuncs) +} diff --git a/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/zz_generated.conversion.go b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/zz_generated.conversion.go new file mode 100644 index 000000000..66f0d13c3 --- /dev/null +++ b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/zz_generated.conversion.go @@ -0,0 +1,450 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 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. +*/ + +// Code generated by conversion-gen. DO NOT EDIT. + +package v1 + +import ( + unsafe "unsafe" + + rbac "github.com/operator-framework/operator-controller/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + conversion "k8s.io/apimachinery/pkg/conversion" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +func init() { + localSchemeBuilder.Register(RegisterConversions) +} + +// RegisterConversions adds conversion functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*rbacv1.AggregationRule)(nil), (*rbac.AggregationRule)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_AggregationRule_To_rbac_AggregationRule(a.(*rbacv1.AggregationRule), b.(*rbac.AggregationRule), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbac.AggregationRule)(nil), (*rbacv1.AggregationRule)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_rbac_AggregationRule_To_v1_AggregationRule(a.(*rbac.AggregationRule), b.(*rbacv1.AggregationRule), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbacv1.ClusterRole)(nil), (*rbac.ClusterRole)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_ClusterRole_To_rbac_ClusterRole(a.(*rbacv1.ClusterRole), b.(*rbac.ClusterRole), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbac.ClusterRole)(nil), (*rbacv1.ClusterRole)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_rbac_ClusterRole_To_v1_ClusterRole(a.(*rbac.ClusterRole), b.(*rbacv1.ClusterRole), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbacv1.ClusterRoleBinding)(nil), (*rbac.ClusterRoleBinding)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_ClusterRoleBinding_To_rbac_ClusterRoleBinding(a.(*rbacv1.ClusterRoleBinding), b.(*rbac.ClusterRoleBinding), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbac.ClusterRoleBinding)(nil), (*rbacv1.ClusterRoleBinding)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_rbac_ClusterRoleBinding_To_v1_ClusterRoleBinding(a.(*rbac.ClusterRoleBinding), b.(*rbacv1.ClusterRoleBinding), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbacv1.ClusterRoleBindingList)(nil), (*rbac.ClusterRoleBindingList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_ClusterRoleBindingList_To_rbac_ClusterRoleBindingList(a.(*rbacv1.ClusterRoleBindingList), b.(*rbac.ClusterRoleBindingList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbac.ClusterRoleBindingList)(nil), (*rbacv1.ClusterRoleBindingList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_rbac_ClusterRoleBindingList_To_v1_ClusterRoleBindingList(a.(*rbac.ClusterRoleBindingList), b.(*rbacv1.ClusterRoleBindingList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbacv1.ClusterRoleList)(nil), (*rbac.ClusterRoleList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_ClusterRoleList_To_rbac_ClusterRoleList(a.(*rbacv1.ClusterRoleList), b.(*rbac.ClusterRoleList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbac.ClusterRoleList)(nil), (*rbacv1.ClusterRoleList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_rbac_ClusterRoleList_To_v1_ClusterRoleList(a.(*rbac.ClusterRoleList), b.(*rbacv1.ClusterRoleList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbacv1.PolicyRule)(nil), (*rbac.PolicyRule)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_PolicyRule_To_rbac_PolicyRule(a.(*rbacv1.PolicyRule), b.(*rbac.PolicyRule), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbac.PolicyRule)(nil), (*rbacv1.PolicyRule)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_rbac_PolicyRule_To_v1_PolicyRule(a.(*rbac.PolicyRule), b.(*rbacv1.PolicyRule), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbacv1.Role)(nil), (*rbac.Role)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_Role_To_rbac_Role(a.(*rbacv1.Role), b.(*rbac.Role), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbac.Role)(nil), (*rbacv1.Role)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_rbac_Role_To_v1_Role(a.(*rbac.Role), b.(*rbacv1.Role), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbacv1.RoleBinding)(nil), (*rbac.RoleBinding)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_RoleBinding_To_rbac_RoleBinding(a.(*rbacv1.RoleBinding), b.(*rbac.RoleBinding), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbac.RoleBinding)(nil), (*rbacv1.RoleBinding)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_rbac_RoleBinding_To_v1_RoleBinding(a.(*rbac.RoleBinding), b.(*rbacv1.RoleBinding), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbacv1.RoleBindingList)(nil), (*rbac.RoleBindingList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_RoleBindingList_To_rbac_RoleBindingList(a.(*rbacv1.RoleBindingList), b.(*rbac.RoleBindingList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbac.RoleBindingList)(nil), (*rbacv1.RoleBindingList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_rbac_RoleBindingList_To_v1_RoleBindingList(a.(*rbac.RoleBindingList), b.(*rbacv1.RoleBindingList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbacv1.RoleList)(nil), (*rbac.RoleList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_RoleList_To_rbac_RoleList(a.(*rbacv1.RoleList), b.(*rbac.RoleList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbac.RoleList)(nil), (*rbacv1.RoleList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_rbac_RoleList_To_v1_RoleList(a.(*rbac.RoleList), b.(*rbacv1.RoleList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbacv1.RoleRef)(nil), (*rbac.RoleRef)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_RoleRef_To_rbac_RoleRef(a.(*rbacv1.RoleRef), b.(*rbac.RoleRef), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbac.RoleRef)(nil), (*rbacv1.RoleRef)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_rbac_RoleRef_To_v1_RoleRef(a.(*rbac.RoleRef), b.(*rbacv1.RoleRef), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbacv1.Subject)(nil), (*rbac.Subject)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_Subject_To_rbac_Subject(a.(*rbacv1.Subject), b.(*rbac.Subject), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*rbac.Subject)(nil), (*rbacv1.Subject)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_rbac_Subject_To_v1_Subject(a.(*rbac.Subject), b.(*rbacv1.Subject), scope) + }); err != nil { + return err + } + return nil +} + +func autoConvert_v1_AggregationRule_To_rbac_AggregationRule(in *rbacv1.AggregationRule, out *rbac.AggregationRule, s conversion.Scope) error { + out.ClusterRoleSelectors = *(*[]metav1.LabelSelector)(unsafe.Pointer(&in.ClusterRoleSelectors)) + return nil +} + +// Convert_v1_AggregationRule_To_rbac_AggregationRule is an autogenerated conversion function. +func Convert_v1_AggregationRule_To_rbac_AggregationRule(in *rbacv1.AggregationRule, out *rbac.AggregationRule, s conversion.Scope) error { + return autoConvert_v1_AggregationRule_To_rbac_AggregationRule(in, out, s) +} + +func autoConvert_rbac_AggregationRule_To_v1_AggregationRule(in *rbac.AggregationRule, out *rbacv1.AggregationRule, s conversion.Scope) error { + out.ClusterRoleSelectors = *(*[]metav1.LabelSelector)(unsafe.Pointer(&in.ClusterRoleSelectors)) + return nil +} + +// Convert_rbac_AggregationRule_To_v1_AggregationRule is an autogenerated conversion function. +func Convert_rbac_AggregationRule_To_v1_AggregationRule(in *rbac.AggregationRule, out *rbacv1.AggregationRule, s conversion.Scope) error { + return autoConvert_rbac_AggregationRule_To_v1_AggregationRule(in, out, s) +} + +func autoConvert_v1_ClusterRole_To_rbac_ClusterRole(in *rbacv1.ClusterRole, out *rbac.ClusterRole, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + out.Rules = *(*[]rbac.PolicyRule)(unsafe.Pointer(&in.Rules)) + out.AggregationRule = (*rbac.AggregationRule)(unsafe.Pointer(in.AggregationRule)) + return nil +} + +// Convert_v1_ClusterRole_To_rbac_ClusterRole is an autogenerated conversion function. +func Convert_v1_ClusterRole_To_rbac_ClusterRole(in *rbacv1.ClusterRole, out *rbac.ClusterRole, s conversion.Scope) error { + return autoConvert_v1_ClusterRole_To_rbac_ClusterRole(in, out, s) +} + +func autoConvert_rbac_ClusterRole_To_v1_ClusterRole(in *rbac.ClusterRole, out *rbacv1.ClusterRole, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + out.Rules = *(*[]rbacv1.PolicyRule)(unsafe.Pointer(&in.Rules)) + out.AggregationRule = (*rbacv1.AggregationRule)(unsafe.Pointer(in.AggregationRule)) + return nil +} + +// Convert_rbac_ClusterRole_To_v1_ClusterRole is an autogenerated conversion function. +func Convert_rbac_ClusterRole_To_v1_ClusterRole(in *rbac.ClusterRole, out *rbacv1.ClusterRole, s conversion.Scope) error { + return autoConvert_rbac_ClusterRole_To_v1_ClusterRole(in, out, s) +} + +func autoConvert_v1_ClusterRoleBinding_To_rbac_ClusterRoleBinding(in *rbacv1.ClusterRoleBinding, out *rbac.ClusterRoleBinding, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + out.Subjects = *(*[]rbac.Subject)(unsafe.Pointer(&in.Subjects)) + if err := Convert_v1_RoleRef_To_rbac_RoleRef(&in.RoleRef, &out.RoleRef, s); err != nil { + return err + } + return nil +} + +// Convert_v1_ClusterRoleBinding_To_rbac_ClusterRoleBinding is an autogenerated conversion function. +func Convert_v1_ClusterRoleBinding_To_rbac_ClusterRoleBinding(in *rbacv1.ClusterRoleBinding, out *rbac.ClusterRoleBinding, s conversion.Scope) error { + return autoConvert_v1_ClusterRoleBinding_To_rbac_ClusterRoleBinding(in, out, s) +} + +func autoConvert_rbac_ClusterRoleBinding_To_v1_ClusterRoleBinding(in *rbac.ClusterRoleBinding, out *rbacv1.ClusterRoleBinding, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + out.Subjects = *(*[]rbacv1.Subject)(unsafe.Pointer(&in.Subjects)) + if err := Convert_rbac_RoleRef_To_v1_RoleRef(&in.RoleRef, &out.RoleRef, s); err != nil { + return err + } + return nil +} + +// Convert_rbac_ClusterRoleBinding_To_v1_ClusterRoleBinding is an autogenerated conversion function. +func Convert_rbac_ClusterRoleBinding_To_v1_ClusterRoleBinding(in *rbac.ClusterRoleBinding, out *rbacv1.ClusterRoleBinding, s conversion.Scope) error { + return autoConvert_rbac_ClusterRoleBinding_To_v1_ClusterRoleBinding(in, out, s) +} + +func autoConvert_v1_ClusterRoleBindingList_To_rbac_ClusterRoleBindingList(in *rbacv1.ClusterRoleBindingList, out *rbac.ClusterRoleBindingList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]rbac.ClusterRoleBinding)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v1_ClusterRoleBindingList_To_rbac_ClusterRoleBindingList is an autogenerated conversion function. +func Convert_v1_ClusterRoleBindingList_To_rbac_ClusterRoleBindingList(in *rbacv1.ClusterRoleBindingList, out *rbac.ClusterRoleBindingList, s conversion.Scope) error { + return autoConvert_v1_ClusterRoleBindingList_To_rbac_ClusterRoleBindingList(in, out, s) +} + +func autoConvert_rbac_ClusterRoleBindingList_To_v1_ClusterRoleBindingList(in *rbac.ClusterRoleBindingList, out *rbacv1.ClusterRoleBindingList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]rbacv1.ClusterRoleBinding)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_rbac_ClusterRoleBindingList_To_v1_ClusterRoleBindingList is an autogenerated conversion function. +func Convert_rbac_ClusterRoleBindingList_To_v1_ClusterRoleBindingList(in *rbac.ClusterRoleBindingList, out *rbacv1.ClusterRoleBindingList, s conversion.Scope) error { + return autoConvert_rbac_ClusterRoleBindingList_To_v1_ClusterRoleBindingList(in, out, s) +} + +func autoConvert_v1_ClusterRoleList_To_rbac_ClusterRoleList(in *rbacv1.ClusterRoleList, out *rbac.ClusterRoleList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]rbac.ClusterRole)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v1_ClusterRoleList_To_rbac_ClusterRoleList is an autogenerated conversion function. +func Convert_v1_ClusterRoleList_To_rbac_ClusterRoleList(in *rbacv1.ClusterRoleList, out *rbac.ClusterRoleList, s conversion.Scope) error { + return autoConvert_v1_ClusterRoleList_To_rbac_ClusterRoleList(in, out, s) +} + +func autoConvert_rbac_ClusterRoleList_To_v1_ClusterRoleList(in *rbac.ClusterRoleList, out *rbacv1.ClusterRoleList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]rbacv1.ClusterRole)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_rbac_ClusterRoleList_To_v1_ClusterRoleList is an autogenerated conversion function. +func Convert_rbac_ClusterRoleList_To_v1_ClusterRoleList(in *rbac.ClusterRoleList, out *rbacv1.ClusterRoleList, s conversion.Scope) error { + return autoConvert_rbac_ClusterRoleList_To_v1_ClusterRoleList(in, out, s) +} + +func autoConvert_v1_PolicyRule_To_rbac_PolicyRule(in *rbacv1.PolicyRule, out *rbac.PolicyRule, s conversion.Scope) error { + out.Verbs = *(*[]string)(unsafe.Pointer(&in.Verbs)) + out.APIGroups = *(*[]string)(unsafe.Pointer(&in.APIGroups)) + out.Resources = *(*[]string)(unsafe.Pointer(&in.Resources)) + out.ResourceNames = *(*[]string)(unsafe.Pointer(&in.ResourceNames)) + out.NonResourceURLs = *(*[]string)(unsafe.Pointer(&in.NonResourceURLs)) + return nil +} + +// Convert_v1_PolicyRule_To_rbac_PolicyRule is an autogenerated conversion function. +func Convert_v1_PolicyRule_To_rbac_PolicyRule(in *rbacv1.PolicyRule, out *rbac.PolicyRule, s conversion.Scope) error { + return autoConvert_v1_PolicyRule_To_rbac_PolicyRule(in, out, s) +} + +func autoConvert_rbac_PolicyRule_To_v1_PolicyRule(in *rbac.PolicyRule, out *rbacv1.PolicyRule, s conversion.Scope) error { + out.Verbs = *(*[]string)(unsafe.Pointer(&in.Verbs)) + out.APIGroups = *(*[]string)(unsafe.Pointer(&in.APIGroups)) + out.Resources = *(*[]string)(unsafe.Pointer(&in.Resources)) + out.ResourceNames = *(*[]string)(unsafe.Pointer(&in.ResourceNames)) + out.NonResourceURLs = *(*[]string)(unsafe.Pointer(&in.NonResourceURLs)) + return nil +} + +// Convert_rbac_PolicyRule_To_v1_PolicyRule is an autogenerated conversion function. +func Convert_rbac_PolicyRule_To_v1_PolicyRule(in *rbac.PolicyRule, out *rbacv1.PolicyRule, s conversion.Scope) error { + return autoConvert_rbac_PolicyRule_To_v1_PolicyRule(in, out, s) +} + +func autoConvert_v1_Role_To_rbac_Role(in *rbacv1.Role, out *rbac.Role, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + out.Rules = *(*[]rbac.PolicyRule)(unsafe.Pointer(&in.Rules)) + return nil +} + +// Convert_v1_Role_To_rbac_Role is an autogenerated conversion function. +func Convert_v1_Role_To_rbac_Role(in *rbacv1.Role, out *rbac.Role, s conversion.Scope) error { + return autoConvert_v1_Role_To_rbac_Role(in, out, s) +} + +func autoConvert_rbac_Role_To_v1_Role(in *rbac.Role, out *rbacv1.Role, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + out.Rules = *(*[]rbacv1.PolicyRule)(unsafe.Pointer(&in.Rules)) + return nil +} + +// Convert_rbac_Role_To_v1_Role is an autogenerated conversion function. +func Convert_rbac_Role_To_v1_Role(in *rbac.Role, out *rbacv1.Role, s conversion.Scope) error { + return autoConvert_rbac_Role_To_v1_Role(in, out, s) +} + +func autoConvert_v1_RoleBinding_To_rbac_RoleBinding(in *rbacv1.RoleBinding, out *rbac.RoleBinding, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + out.Subjects = *(*[]rbac.Subject)(unsafe.Pointer(&in.Subjects)) + if err := Convert_v1_RoleRef_To_rbac_RoleRef(&in.RoleRef, &out.RoleRef, s); err != nil { + return err + } + return nil +} + +// Convert_v1_RoleBinding_To_rbac_RoleBinding is an autogenerated conversion function. +func Convert_v1_RoleBinding_To_rbac_RoleBinding(in *rbacv1.RoleBinding, out *rbac.RoleBinding, s conversion.Scope) error { + return autoConvert_v1_RoleBinding_To_rbac_RoleBinding(in, out, s) +} + +func autoConvert_rbac_RoleBinding_To_v1_RoleBinding(in *rbac.RoleBinding, out *rbacv1.RoleBinding, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + out.Subjects = *(*[]rbacv1.Subject)(unsafe.Pointer(&in.Subjects)) + if err := Convert_rbac_RoleRef_To_v1_RoleRef(&in.RoleRef, &out.RoleRef, s); err != nil { + return err + } + return nil +} + +// Convert_rbac_RoleBinding_To_v1_RoleBinding is an autogenerated conversion function. +func Convert_rbac_RoleBinding_To_v1_RoleBinding(in *rbac.RoleBinding, out *rbacv1.RoleBinding, s conversion.Scope) error { + return autoConvert_rbac_RoleBinding_To_v1_RoleBinding(in, out, s) +} + +func autoConvert_v1_RoleBindingList_To_rbac_RoleBindingList(in *rbacv1.RoleBindingList, out *rbac.RoleBindingList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]rbac.RoleBinding)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v1_RoleBindingList_To_rbac_RoleBindingList is an autogenerated conversion function. +func Convert_v1_RoleBindingList_To_rbac_RoleBindingList(in *rbacv1.RoleBindingList, out *rbac.RoleBindingList, s conversion.Scope) error { + return autoConvert_v1_RoleBindingList_To_rbac_RoleBindingList(in, out, s) +} + +func autoConvert_rbac_RoleBindingList_To_v1_RoleBindingList(in *rbac.RoleBindingList, out *rbacv1.RoleBindingList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]rbacv1.RoleBinding)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_rbac_RoleBindingList_To_v1_RoleBindingList is an autogenerated conversion function. +func Convert_rbac_RoleBindingList_To_v1_RoleBindingList(in *rbac.RoleBindingList, out *rbacv1.RoleBindingList, s conversion.Scope) error { + return autoConvert_rbac_RoleBindingList_To_v1_RoleBindingList(in, out, s) +} + +func autoConvert_v1_RoleList_To_rbac_RoleList(in *rbacv1.RoleList, out *rbac.RoleList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]rbac.Role)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v1_RoleList_To_rbac_RoleList is an autogenerated conversion function. +func Convert_v1_RoleList_To_rbac_RoleList(in *rbacv1.RoleList, out *rbac.RoleList, s conversion.Scope) error { + return autoConvert_v1_RoleList_To_rbac_RoleList(in, out, s) +} + +func autoConvert_rbac_RoleList_To_v1_RoleList(in *rbac.RoleList, out *rbacv1.RoleList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]rbacv1.Role)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_rbac_RoleList_To_v1_RoleList is an autogenerated conversion function. +func Convert_rbac_RoleList_To_v1_RoleList(in *rbac.RoleList, out *rbacv1.RoleList, s conversion.Scope) error { + return autoConvert_rbac_RoleList_To_v1_RoleList(in, out, s) +} + +func autoConvert_v1_RoleRef_To_rbac_RoleRef(in *rbacv1.RoleRef, out *rbac.RoleRef, s conversion.Scope) error { + out.APIGroup = in.APIGroup + out.Kind = in.Kind + out.Name = in.Name + return nil +} + +// Convert_v1_RoleRef_To_rbac_RoleRef is an autogenerated conversion function. +func Convert_v1_RoleRef_To_rbac_RoleRef(in *rbacv1.RoleRef, out *rbac.RoleRef, s conversion.Scope) error { + return autoConvert_v1_RoleRef_To_rbac_RoleRef(in, out, s) +} + +func autoConvert_rbac_RoleRef_To_v1_RoleRef(in *rbac.RoleRef, out *rbacv1.RoleRef, s conversion.Scope) error { + out.APIGroup = in.APIGroup + out.Kind = in.Kind + out.Name = in.Name + return nil +} + +// Convert_rbac_RoleRef_To_v1_RoleRef is an autogenerated conversion function. +func Convert_rbac_RoleRef_To_v1_RoleRef(in *rbac.RoleRef, out *rbacv1.RoleRef, s conversion.Scope) error { + return autoConvert_rbac_RoleRef_To_v1_RoleRef(in, out, s) +} + +func autoConvert_v1_Subject_To_rbac_Subject(in *rbacv1.Subject, out *rbac.Subject, s conversion.Scope) error { + out.Kind = in.Kind + out.APIGroup = in.APIGroup + out.Name = in.Name + out.Namespace = in.Namespace + return nil +} + +// Convert_v1_Subject_To_rbac_Subject is an autogenerated conversion function. +func Convert_v1_Subject_To_rbac_Subject(in *rbacv1.Subject, out *rbac.Subject, s conversion.Scope) error { + return autoConvert_v1_Subject_To_rbac_Subject(in, out, s) +} + +func autoConvert_rbac_Subject_To_v1_Subject(in *rbac.Subject, out *rbacv1.Subject, s conversion.Scope) error { + out.Kind = in.Kind + out.APIGroup = in.APIGroup + out.Name = in.Name + out.Namespace = in.Namespace + return nil +} + +// Convert_rbac_Subject_To_v1_Subject is an autogenerated conversion function. +func Convert_rbac_Subject_To_v1_Subject(in *rbac.Subject, out *rbacv1.Subject, s conversion.Scope) error { + return autoConvert_rbac_Subject_To_v1_Subject(in, out, s) +} diff --git a/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/zz_generated.defaults.go b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/zz_generated.defaults.go new file mode 100644 index 000000000..734e76f3e --- /dev/null +++ b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1/zz_generated.defaults.go @@ -0,0 +1,68 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 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. +*/ + +// Code generated by defaulter-gen. DO NOT EDIT. + +package v1 + +import ( + rbacv1 "k8s.io/api/rbac/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// RegisterDefaults adds defaulters functions to the given scheme. +// Public to allow building arbitrary schemes. +// All generated defaulters are covering - they call all nested defaulters. +func RegisterDefaults(scheme *runtime.Scheme) error { + scheme.AddTypeDefaultingFunc(&rbacv1.ClusterRoleBinding{}, func(obj interface{}) { SetObjectDefaults_ClusterRoleBinding(obj.(*rbacv1.ClusterRoleBinding)) }) + scheme.AddTypeDefaultingFunc(&rbacv1.ClusterRoleBindingList{}, func(obj interface{}) { SetObjectDefaults_ClusterRoleBindingList(obj.(*rbacv1.ClusterRoleBindingList)) }) + scheme.AddTypeDefaultingFunc(&rbacv1.RoleBinding{}, func(obj interface{}) { SetObjectDefaults_RoleBinding(obj.(*rbacv1.RoleBinding)) }) + scheme.AddTypeDefaultingFunc(&rbacv1.RoleBindingList{}, func(obj interface{}) { SetObjectDefaults_RoleBindingList(obj.(*rbacv1.RoleBindingList)) }) + return nil +} + +func SetObjectDefaults_ClusterRoleBinding(in *rbacv1.ClusterRoleBinding) { + SetDefaults_ClusterRoleBinding(in) + for i := range in.Subjects { + a := &in.Subjects[i] + SetDefaults_Subject(a) + } +} + +func SetObjectDefaults_ClusterRoleBindingList(in *rbacv1.ClusterRoleBindingList) { + for i := range in.Items { + a := &in.Items[i] + SetObjectDefaults_ClusterRoleBinding(a) + } +} + +func SetObjectDefaults_RoleBinding(in *rbacv1.RoleBinding) { + SetDefaults_RoleBinding(in) + for i := range in.Subjects { + a := &in.Subjects[i] + SetDefaults_Subject(a) + } +} + +func SetObjectDefaults_RoleBindingList(in *rbacv1.RoleBindingList) { + for i := range in.Items { + a := &in.Items[i] + SetObjectDefaults_RoleBinding(a) + } +} diff --git a/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/zz_generated.deepcopy.go b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/zz_generated.deepcopy.go new file mode 100644 index 000000000..0f7023a2d --- /dev/null +++ b/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/zz_generated.deepcopy.go @@ -0,0 +1,412 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 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. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package rbac + +import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AggregationRule) DeepCopyInto(out *AggregationRule) { + *out = *in + if in.ClusterRoleSelectors != nil { + in, out := &in.ClusterRoleSelectors, &out.ClusterRoleSelectors + *out = make([]v1.LabelSelector, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AggregationRule. +func (in *AggregationRule) DeepCopy() *AggregationRule { + if in == nil { + return nil + } + out := new(AggregationRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterRole) DeepCopyInto(out *ClusterRole) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]PolicyRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AggregationRule != nil { + in, out := &in.AggregationRule, &out.AggregationRule + *out = new(AggregationRule) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterRole. +func (in *ClusterRole) DeepCopy() *ClusterRole { + if in == nil { + return nil + } + out := new(ClusterRole) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterRole) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterRoleBinding) DeepCopyInto(out *ClusterRoleBinding) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Subjects != nil { + in, out := &in.Subjects, &out.Subjects + *out = make([]Subject, len(*in)) + copy(*out, *in) + } + out.RoleRef = in.RoleRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterRoleBinding. +func (in *ClusterRoleBinding) DeepCopy() *ClusterRoleBinding { + if in == nil { + return nil + } + out := new(ClusterRoleBinding) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterRoleBinding) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterRoleBindingList) DeepCopyInto(out *ClusterRoleBindingList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterRoleBinding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterRoleBindingList. +func (in *ClusterRoleBindingList) DeepCopy() *ClusterRoleBindingList { + if in == nil { + return nil + } + out := new(ClusterRoleBindingList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterRoleBindingList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterRoleList) DeepCopyInto(out *ClusterRoleList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterRole, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterRoleList. +func (in *ClusterRoleList) DeepCopy() *ClusterRoleList { + if in == nil { + return nil + } + out := new(ClusterRoleList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterRoleList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PolicyRule) DeepCopyInto(out *PolicyRule) { + *out = *in + if in.Verbs != nil { + in, out := &in.Verbs, &out.Verbs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.APIGroups != nil { + in, out := &in.APIGroups, &out.APIGroups + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ResourceNames != nil { + in, out := &in.ResourceNames, &out.ResourceNames + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.NonResourceURLs != nil { + in, out := &in.NonResourceURLs, &out.NonResourceURLs + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyRule. +func (in *PolicyRule) DeepCopy() *PolicyRule { + if in == nil { + return nil + } + out := new(PolicyRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Role) DeepCopyInto(out *Role) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]PolicyRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Role. +func (in *Role) DeepCopy() *Role { + if in == nil { + return nil + } + out := new(Role) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Role) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoleBinding) DeepCopyInto(out *RoleBinding) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Subjects != nil { + in, out := &in.Subjects, &out.Subjects + *out = make([]Subject, len(*in)) + copy(*out, *in) + } + out.RoleRef = in.RoleRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleBinding. +func (in *RoleBinding) DeepCopy() *RoleBinding { + if in == nil { + return nil + } + out := new(RoleBinding) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RoleBinding) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoleBindingList) DeepCopyInto(out *RoleBindingList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RoleBinding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleBindingList. +func (in *RoleBindingList) DeepCopy() *RoleBindingList { + if in == nil { + return nil + } + out := new(RoleBindingList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RoleBindingList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoleList) DeepCopyInto(out *RoleList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Role, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleList. +func (in *RoleList) DeepCopy() *RoleList { + if in == nil { + return nil + } + out := new(RoleList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RoleList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoleRef) DeepCopyInto(out *RoleRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleRef. +func (in *RoleRef) DeepCopy() *RoleRef { + if in == nil { + return nil + } + out := new(RoleRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in SortableRuleSlice) DeepCopyInto(out *SortableRuleSlice) { + { + in := &in + *out = make(SortableRuleSlice, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SortableRuleSlice. +func (in SortableRuleSlice) DeepCopy() SortableRuleSlice { + if in == nil { + return nil + } + out := new(SortableRuleSlice) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Subject) DeepCopyInto(out *Subject) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Subject. +func (in *Subject) DeepCopy() *Subject { + if in == nil { + return nil + } + out := new(Subject) + in.DeepCopyInto(out) + return out +} diff --git a/internal/operator-controller/authorization/internal/kubernetes/pkg/registry/rbac/escalation_check.go b/internal/operator-controller/authorization/internal/kubernetes/pkg/registry/rbac/escalation_check.go new file mode 100644 index 000000000..1534f43cc --- /dev/null +++ b/internal/operator-controller/authorization/internal/kubernetes/pkg/registry/rbac/escalation_check.go @@ -0,0 +1,146 @@ +/* +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 rbac + +import ( + "context" + "fmt" + + "github.com/operator-framework/operator-controller/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac" + "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" +) + +// EscalationAllowed checks if the user associated with the context is a superuser +func EscalationAllowed(ctx context.Context) bool { + u, ok := genericapirequest.UserFrom(ctx) + if !ok { + return false + } + + // system:masters is special because the API server uses it for privileged loopback connections + // therefore we know that a member of system:masters can always do anything + for _, group := range u.GetGroups() { + if group == user.SystemPrivilegedGroup { + return true + } + } + + return false +} + +var roleResources = map[schema.GroupResource]bool{ + rbac.SchemeGroupVersion.WithResource("clusterroles").GroupResource(): true, + rbac.SchemeGroupVersion.WithResource("roles").GroupResource(): true, +} + +// RoleEscalationAuthorized checks if the user associated with the context is explicitly authorized to escalate the role resource associated with the context +func RoleEscalationAuthorized(ctx context.Context, a authorizer.Authorizer) bool { + if a == nil { + return false + } + + user, ok := genericapirequest.UserFrom(ctx) + if !ok { + return false + } + + requestInfo, ok := genericapirequest.RequestInfoFrom(ctx) + if !ok { + return false + } + + if !requestInfo.IsResourceRequest { + return false + } + + requestResource := schema.GroupResource{Group: requestInfo.APIGroup, Resource: requestInfo.Resource} + if !roleResources[requestResource] { + return false + } + + attrs := authorizer.AttributesRecord{ + User: user, + Verb: "escalate", + APIGroup: requestInfo.APIGroup, + APIVersion: "*", + Resource: requestInfo.Resource, + Name: requestInfo.Name, + Namespace: requestInfo.Namespace, + ResourceRequest: true, + } + + decision, _, err := a.Authorize(ctx, attrs) + if err != nil { + utilruntime.HandleError(fmt.Errorf( + "error authorizing user %#v to escalate %#v named %q in namespace %q: %v", + user, requestResource, requestInfo.Name, requestInfo.Namespace, err, + )) + } + return decision == authorizer.DecisionAllow +} + +// BindingAuthorized returns true if the user associated with the context is explicitly authorized to bind the specified roleRef +func BindingAuthorized(ctx context.Context, roleRef rbac.RoleRef, bindingNamespace string, a authorizer.Authorizer) bool { + if a == nil { + return false + } + + user, ok := genericapirequest.UserFrom(ctx) + if !ok { + return false + } + + attrs := authorizer.AttributesRecord{ + User: user, + Verb: "bind", + // check against the namespace where the binding is being created (or the empty namespace for clusterrolebindings). + // this allows delegation to bind particular clusterroles in rolebindings within particular namespaces, + // and to authorize binding a clusterrole across all namespaces in a clusterrolebinding. + Namespace: bindingNamespace, + ResourceRequest: true, + } + + // This occurs after defaulting and conversion, so values pulled from the roleRef won't change + // Invalid APIGroup or Name values will fail validation + switch roleRef.Kind { + case "ClusterRole": + attrs.APIGroup = roleRef.APIGroup + attrs.APIVersion = "*" + attrs.Resource = "clusterroles" + attrs.Name = roleRef.Name + case "Role": + attrs.APIGroup = roleRef.APIGroup + attrs.APIVersion = "*" + attrs.Resource = "roles" + attrs.Name = roleRef.Name + default: + return false + } + + decision, _, err := a.Authorize(ctx, attrs) + if err != nil { + utilruntime.HandleError(fmt.Errorf( + "error authorizing user %#v to bind %#v in namespace %s: %v", + user, roleRef, bindingNamespace, err, + )) + } + return decision == authorizer.DecisionAllow +} diff --git a/internal/operator-controller/authorization/internal/kubernetes/pkg/registry/rbac/validation/policy_compact.go b/internal/operator-controller/authorization/internal/kubernetes/pkg/registry/rbac/validation/policy_compact.go new file mode 100644 index 000000000..60adcf9e8 --- /dev/null +++ b/internal/operator-controller/authorization/internal/kubernetes/pkg/registry/rbac/validation/policy_compact.go @@ -0,0 +1,96 @@ +/* +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 validation + +import ( + "reflect" + + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/util/sets" +) + +type simpleResource struct { + Group string + Resource string + ResourceNameExist bool + ResourceName string +} + +// CompactRules combines rules that contain a single APIGroup/Resource, differ only by verb, and contain no other attributes. +// this is a fast check, and works well with the decomposed "missing rules" list from a Covers check. +func CompactRules(rules []rbacv1.PolicyRule) ([]rbacv1.PolicyRule, error) { + compacted := make([]rbacv1.PolicyRule, 0, len(rules)) + + simpleRules := map[simpleResource]*rbacv1.PolicyRule{} + for _, rule := range rules { + if resource, isSimple := isSimpleResourceRule(&rule); isSimple { + if existingRule, ok := simpleRules[resource]; ok { + // Add the new verbs to the existing simple resource rule + if existingRule.Verbs == nil { + existingRule.Verbs = []string{} + } + existingRule.Verbs = append(existingRule.Verbs, rule.Verbs...) + } else { + // Copy the rule to accumulate matching simple resource rules into + simpleRules[resource] = rule.DeepCopy() + } + } else { + compacted = append(compacted, rule) + } + } + + // Once we've consolidated the simple resource rules, add them to the compacted list + for _, simpleRule := range simpleRules { + verbSet := sets.New[string](simpleRule.Verbs...) + if verbSet.Has("*") { + simpleRule.Verbs = []string{"*"} + } else { + simpleRule.Verbs = sets.List(verbSet) + } + compacted = append(compacted, *simpleRule) + } + + return compacted, nil +} + +// isSimpleResourceRule returns true if the given rule contains verbs, a single resource, a single API group, at most one Resource Name, and no other values +func isSimpleResourceRule(rule *rbacv1.PolicyRule) (simpleResource, bool) { + resource := simpleResource{} + + // If we have "complex" rule attributes, return early without allocations or expensive comparisons + if len(rule.ResourceNames) > 1 || len(rule.NonResourceURLs) > 0 { + return resource, false + } + // If we have multiple api groups or resources, return early + if len(rule.APIGroups) != 1 || len(rule.Resources) != 1 { + return resource, false + } + + // Test if this rule only contains APIGroups/Resources/Verbs/ResourceNames + simpleRule := &rbacv1.PolicyRule{APIGroups: rule.APIGroups, Resources: rule.Resources, Verbs: rule.Verbs, ResourceNames: rule.ResourceNames} + if !reflect.DeepEqual(simpleRule, rule) { + return resource, false + } + + if len(rule.ResourceNames) == 0 { + resource = simpleResource{Group: rule.APIGroups[0], Resource: rule.Resources[0], ResourceNameExist: false} + } else { + resource = simpleResource{Group: rule.APIGroups[0], Resource: rule.Resources[0], ResourceNameExist: true, ResourceName: rule.ResourceNames[0]} + } + + return resource, true +} diff --git a/internal/operator-controller/authorization/internal/kubernetes/pkg/registry/rbac/validation/rule.go b/internal/operator-controller/authorization/internal/kubernetes/pkg/registry/rbac/validation/rule.go new file mode 100644 index 000000000..ad79998fd --- /dev/null +++ b/internal/operator-controller/authorization/internal/kubernetes/pkg/registry/rbac/validation/rule.go @@ -0,0 +1,384 @@ +/* +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 ( + "context" + "errors" + "fmt" + "strings" + + rbacv1 "k8s.io/api/rbac/v1" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apiserver/pkg/authentication/serviceaccount" + "k8s.io/apiserver/pkg/authentication/user" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/component-helpers/auth/rbac/validation" + "k8s.io/klog/v2" + + rbacv1helpers "github.com/operator-framework/operator-controller/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1" +) + +type AuthorizationRuleResolver interface { + // GetRoleReferenceRules attempts to resolve the role reference of a RoleBinding or ClusterRoleBinding. The passed namespace should be the namespace + // of the role binding, the empty string if a cluster role binding. + GetRoleReferenceRules(ctx context.Context, roleRef rbacv1.RoleRef, namespace string) ([]rbacv1.PolicyRule, error) + + // RulesFor returns the list of rules that apply to a given user in a given namespace and error. If an error is returned, the slice of + // PolicyRules may not be complete, but it contains all retrievable rules. This is done because policy rules are purely additive and policy determinations + // can be made on the basis of those rules that are found. + RulesFor(ctx context.Context, user user.Info, namespace string) ([]rbacv1.PolicyRule, error) + + // VisitRulesFor invokes visitor() with each rule that applies to a given user in a given namespace, and each error encountered resolving those rules. + // If visitor() returns false, visiting is short-circuited. + VisitRulesFor(ctx context.Context, user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool) +} + +type PrivilegeEscalationError struct { + User user.Info + Namespace string + MissingRules []rbacv1.PolicyRule + RuleResolutionErrors []error +} + +func (e *PrivilegeEscalationError) Error() string { + missingDescriptions := sets.NewString() + for _, missing := range e.MissingRules { + missingDescriptions.Insert(rbacv1helpers.CompactString(missing)) + } + + msg := fmt.Sprintf("user %q (groups=%q) is attempting to grant RBAC permissions not currently held:\n%s", e.User.GetName(), e.User.GetGroups(), strings.Join(missingDescriptions.List(), "\n")) + if len(e.RuleResolutionErrors) > 0 { + msg = msg + fmt.Sprintf("; resolution errors: %v", e.RuleResolutionErrors) + } + + return msg +} + +// ConfirmNoEscalation determines if the roles for a given user in a given namespace encompass the provided role. +func ConfirmNoEscalation(ctx context.Context, ruleResolver AuthorizationRuleResolver, rules []rbacv1.PolicyRule) error { + ruleResolutionErrors := []error{} + + user, ok := genericapirequest.UserFrom(ctx) + if !ok { + return fmt.Errorf("no user on context") + } + namespace, _ := genericapirequest.NamespaceFrom(ctx) + + ownerRules, err := ruleResolver.RulesFor(ctx, user, namespace) + if err != nil { + // As per AuthorizationRuleResolver contract, this may return a non fatal error with an incomplete list of policies. Log the error and continue. + klog.V(1).Infof("non-fatal error getting local rules for %v: %v", user, err) + ruleResolutionErrors = append(ruleResolutionErrors, err) + } + + ownerRightsCover, missingRights := validation.Covers(ownerRules, rules) + if !ownerRightsCover { + compactMissingRights := missingRights + if compact, err := CompactRules(missingRights); err == nil { + compactMissingRights = compact + } + + return &PrivilegeEscalationError{ + User: user, + Namespace: namespace, + MissingRules: compactMissingRights, + RuleResolutionErrors: ruleResolutionErrors, + } + } + return nil +} + +type DefaultRuleResolver struct { + roleGetter RoleGetter + roleBindingLister RoleBindingLister + clusterRoleGetter ClusterRoleGetter + clusterRoleBindingLister ClusterRoleBindingLister +} + +func NewDefaultRuleResolver(roleGetter RoleGetter, roleBindingLister RoleBindingLister, clusterRoleGetter ClusterRoleGetter, clusterRoleBindingLister ClusterRoleBindingLister) *DefaultRuleResolver { + return &DefaultRuleResolver{roleGetter, roleBindingLister, clusterRoleGetter, clusterRoleBindingLister} +} + +type RoleGetter interface { + GetRole(ctx context.Context, namespace, name string) (*rbacv1.Role, error) +} + +type RoleBindingLister interface { + ListRoleBindings(ctx context.Context, namespace string) ([]*rbacv1.RoleBinding, error) +} + +type ClusterRoleGetter interface { + GetClusterRole(ctx context.Context, name string) (*rbacv1.ClusterRole, error) +} + +type ClusterRoleBindingLister interface { + ListClusterRoleBindings(ctx context.Context) ([]*rbacv1.ClusterRoleBinding, error) +} + +func (r *DefaultRuleResolver) RulesFor(ctx context.Context, user user.Info, namespace string) ([]rbacv1.PolicyRule, error) { + visitor := &ruleAccumulator{} + r.VisitRulesFor(ctx, user, namespace, visitor.visit) + return visitor.rules, utilerrors.NewAggregate(visitor.errors) +} + +type ruleAccumulator struct { + rules []rbacv1.PolicyRule + errors []error +} + +func (r *ruleAccumulator) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool { + if rule != nil { + r.rules = append(r.rules, *rule) + } + if err != nil { + r.errors = append(r.errors, err) + } + return true +} + +func describeSubject(s *rbacv1.Subject, bindingNamespace string) string { + switch s.Kind { + case rbacv1.ServiceAccountKind: + if len(s.Namespace) > 0 { + return fmt.Sprintf("%s %q", s.Kind, s.Name+"/"+s.Namespace) + } + return fmt.Sprintf("%s %q", s.Kind, s.Name+"/"+bindingNamespace) + default: + return fmt.Sprintf("%s %q", s.Kind, s.Name) + } +} + +type clusterRoleBindingDescriber struct { + binding *rbacv1.ClusterRoleBinding + subject *rbacv1.Subject +} + +func (d *clusterRoleBindingDescriber) String() string { + return fmt.Sprintf("ClusterRoleBinding %q of %s %q to %s", + d.binding.Name, + d.binding.RoleRef.Kind, + d.binding.RoleRef.Name, + describeSubject(d.subject, ""), + ) +} + +type roleBindingDescriber struct { + binding *rbacv1.RoleBinding + subject *rbacv1.Subject +} + +func (d *roleBindingDescriber) String() string { + return fmt.Sprintf("RoleBinding %q of %s %q to %s", + d.binding.Name+"/"+d.binding.Namespace, + d.binding.RoleRef.Kind, + d.binding.RoleRef.Name, + describeSubject(d.subject, d.binding.Namespace), + ) +} + +func (r *DefaultRuleResolver) VisitRulesFor(ctx context.Context, user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool) { + if clusterRoleBindings, err := r.clusterRoleBindingLister.ListClusterRoleBindings(ctx); err != nil { + if !visitor(nil, nil, err) { + return + } + } else { + sourceDescriber := &clusterRoleBindingDescriber{} + for _, clusterRoleBinding := range clusterRoleBindings { + subjectIndex, applies := appliesTo(user, clusterRoleBinding.Subjects, "") + if !applies { + continue + } + rules, err := r.GetRoleReferenceRules(ctx, clusterRoleBinding.RoleRef, "") + if err != nil { + if !visitor(nil, nil, err) { + return + } + continue + } + sourceDescriber.binding = clusterRoleBinding + sourceDescriber.subject = &clusterRoleBinding.Subjects[subjectIndex] + for i := range rules { + if !visitor(sourceDescriber, &rules[i], nil) { + return + } + } + } + } + + if len(namespace) > 0 { + if roleBindings, err := r.roleBindingLister.ListRoleBindings(ctx, namespace); err != nil { + if !visitor(nil, nil, err) { + return + } + } else { + sourceDescriber := &roleBindingDescriber{} + for _, roleBinding := range roleBindings { + subjectIndex, applies := appliesTo(user, roleBinding.Subjects, namespace) + if !applies { + continue + } + rules, err := r.GetRoleReferenceRules(ctx, roleBinding.RoleRef, namespace) + if err != nil { + if !visitor(nil, nil, err) { + return + } + continue + } + sourceDescriber.binding = roleBinding + sourceDescriber.subject = &roleBinding.Subjects[subjectIndex] + for i := range rules { + if !visitor(sourceDescriber, &rules[i], nil) { + return + } + } + } + } + } +} + +// GetRoleReferenceRules attempts to resolve the RoleBinding or ClusterRoleBinding. +func (r *DefaultRuleResolver) GetRoleReferenceRules(ctx context.Context, roleRef rbacv1.RoleRef, bindingNamespace string) ([]rbacv1.PolicyRule, error) { + switch roleRef.Kind { + case "Role": + role, err := r.roleGetter.GetRole(ctx, bindingNamespace, roleRef.Name) + if err != nil { + return nil, err + } + return role.Rules, nil + + case "ClusterRole": + clusterRole, err := r.clusterRoleGetter.GetClusterRole(ctx, roleRef.Name) + if err != nil { + return nil, err + } + return clusterRole.Rules, nil + + default: + return nil, fmt.Errorf("unsupported role reference kind: %q", roleRef.Kind) + } +} + +// appliesTo returns whether any of the bindingSubjects applies to the specified subject, +// and if true, the index of the first subject that applies +func appliesTo(user user.Info, bindingSubjects []rbacv1.Subject, namespace string) (int, bool) { + for i, bindingSubject := range bindingSubjects { + if appliesToUser(user, bindingSubject, namespace) { + return i, true + } + } + return 0, false +} + +func has(set []string, ele string) bool { + for _, s := range set { + if s == ele { + return true + } + } + return false +} + +func appliesToUser(user user.Info, subject rbacv1.Subject, namespace string) bool { + switch subject.Kind { + case rbacv1.UserKind: + return user.GetName() == subject.Name + + case rbacv1.GroupKind: + return has(user.GetGroups(), subject.Name) + + case rbacv1.ServiceAccountKind: + // default the namespace to namespace we're working in if its available. This allows rolebindings that reference + // SAs in th local namespace to avoid having to qualify them. + saNamespace := namespace + if len(subject.Namespace) > 0 { + saNamespace = subject.Namespace + } + if len(saNamespace) == 0 { + return false + } + // use a more efficient comparison for RBAC checking + return serviceaccount.MatchesUsername(saNamespace, subject.Name, user.GetName()) + default: + return false + } +} + +// NewTestRuleResolver returns a rule resolver from lists of role objects. +func NewTestRuleResolver(roles []*rbacv1.Role, roleBindings []*rbacv1.RoleBinding, clusterRoles []*rbacv1.ClusterRole, clusterRoleBindings []*rbacv1.ClusterRoleBinding) (AuthorizationRuleResolver, *StaticRoles) { + r := StaticRoles{ + roles: roles, + roleBindings: roleBindings, + clusterRoles: clusterRoles, + clusterRoleBindings: clusterRoleBindings, + } + return newMockRuleResolver(&r), &r +} + +func newMockRuleResolver(r *StaticRoles) AuthorizationRuleResolver { + return NewDefaultRuleResolver(r, r, r, r) +} + +// StaticRoles is a rule resolver that resolves from lists of role objects. +type StaticRoles struct { + roles []*rbacv1.Role + roleBindings []*rbacv1.RoleBinding + clusterRoles []*rbacv1.ClusterRole + clusterRoleBindings []*rbacv1.ClusterRoleBinding +} + +func (r *StaticRoles) GetRole(ctx context.Context, namespace, name string) (*rbacv1.Role, error) { + if len(namespace) == 0 { + return nil, errors.New("must provide namespace when getting role") + } + for _, role := range r.roles { + if role.Namespace == namespace && role.Name == name { + return role, nil + } + } + return nil, errors.New("role not found") +} + +func (r *StaticRoles) GetClusterRole(ctx context.Context, name string) (*rbacv1.ClusterRole, error) { + for _, clusterRole := range r.clusterRoles { + if clusterRole.Name == name { + return clusterRole, nil + } + } + return nil, errors.New("clusterrole not found") +} + +func (r *StaticRoles) ListRoleBindings(ctx context.Context, namespace string) ([]*rbacv1.RoleBinding, error) { + if len(namespace) == 0 { + return nil, errors.New("must provide namespace when listing role bindings") + } + + roleBindingList := []*rbacv1.RoleBinding{} + for _, roleBinding := range r.roleBindings { + if roleBinding.Namespace != namespace { + continue + } + // TODO(ericchiang): need to implement label selectors? + roleBindingList = append(roleBindingList, roleBinding) + } + return roleBindingList, nil +} + +func (r *StaticRoles) ListClusterRoleBindings(ctx context.Context) ([]*rbacv1.ClusterRoleBinding, error) { + return r.clusterRoleBindings, nil +} diff --git a/internal/operator-controller/authorization/internal/kubernetes/plugin/pkg/auth/authorizer/rbac/rbac.go b/internal/operator-controller/authorization/internal/kubernetes/plugin/pkg/auth/authorizer/rbac/rbac.go new file mode 100644 index 000000000..d350848d5 --- /dev/null +++ b/internal/operator-controller/authorization/internal/kubernetes/plugin/pkg/auth/authorizer/rbac/rbac.go @@ -0,0 +1,225 @@ +/* +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 rbac implements the authorizer.Authorizer interface using roles base access control. +package rbac + +import ( + "bytes" + "context" + "fmt" + + "k8s.io/klog/v2" + + rbacv1helpers "github.com/operator-framework/operator-controller/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1" + rbacregistryvalidation "github.com/operator-framework/operator-controller/internal/operator-controller/authorization/internal/kubernetes/pkg/registry/rbac/validation" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/labels" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + rbaclisters "k8s.io/client-go/listers/rbac/v1" +) + +type RequestToRuleMapper interface { + // RulesFor returns all known PolicyRules and any errors that happened while locating those rules. + // Any rule returned is still valid, since rules are deny by default. If you can pass with the rules + // supplied, you do not have to fail the request. If you cannot, you should indicate the error along + // with your denial. + RulesFor(ctx context.Context, subject user.Info, namespace string) ([]rbacv1.PolicyRule, error) + + // VisitRulesFor invokes visitor() with each rule that applies to a given user in a given namespace, + // and each error encountered resolving those rules. Rule may be nil if err is non-nil. + // If visitor() returns false, visiting is short-circuited. + VisitRulesFor(ctx context.Context, user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool) +} + +type RBACAuthorizer struct { + authorizationRuleResolver RequestToRuleMapper +} + +// authorizingVisitor short-circuits once allowed, and collects any resolution errors encountered +type authorizingVisitor struct { + requestAttributes authorizer.Attributes + + allowed bool + reason string + errors []error +} + +func (v *authorizingVisitor) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool { + if rule != nil && RuleAllows(v.requestAttributes, rule) { + v.allowed = true + v.reason = fmt.Sprintf("RBAC: allowed by %s", source.String()) + return false + } + if err != nil { + v.errors = append(v.errors, err) + } + return true +} + +func (r *RBACAuthorizer) Authorize(ctx context.Context, requestAttributes authorizer.Attributes) (authorizer.Decision, string, error) { + ruleCheckingVisitor := &authorizingVisitor{requestAttributes: requestAttributes} + + r.authorizationRuleResolver.VisitRulesFor(ctx, requestAttributes.GetUser(), requestAttributes.GetNamespace(), ruleCheckingVisitor.visit) + if ruleCheckingVisitor.allowed { + return authorizer.DecisionAllow, ruleCheckingVisitor.reason, nil + } + + // Build a detailed log of the denial. + // Make the whole block conditional so we don't do a lot of string-building we won't use. + if klogV := klog.V(5); klogV.Enabled() { + var operation string + if requestAttributes.IsResourceRequest() { + b := &bytes.Buffer{} + b.WriteString(`"`) + b.WriteString(requestAttributes.GetVerb()) + b.WriteString(`" resource "`) + b.WriteString(requestAttributes.GetResource()) + if len(requestAttributes.GetAPIGroup()) > 0 { + b.WriteString(`.`) + b.WriteString(requestAttributes.GetAPIGroup()) + } + if len(requestAttributes.GetSubresource()) > 0 { + b.WriteString(`/`) + b.WriteString(requestAttributes.GetSubresource()) + } + b.WriteString(`"`) + if len(requestAttributes.GetName()) > 0 { + b.WriteString(` named "`) + b.WriteString(requestAttributes.GetName()) + b.WriteString(`"`) + } + operation = b.String() + } else { + operation = fmt.Sprintf("%q nonResourceURL %q", requestAttributes.GetVerb(), requestAttributes.GetPath()) + } + + var scope string + if ns := requestAttributes.GetNamespace(); len(ns) > 0 { + scope = fmt.Sprintf("in namespace %q", ns) + } else { + scope = "cluster-wide" + } + + klogV.Infof("RBAC: no rules authorize user %q with groups %q to %s %s", requestAttributes.GetUser().GetName(), requestAttributes.GetUser().GetGroups(), operation, scope) + } + + reason := "" + if len(ruleCheckingVisitor.errors) > 0 { + reason = fmt.Sprintf("RBAC: %v", utilerrors.NewAggregate(ruleCheckingVisitor.errors)) + } + return authorizer.DecisionNoOpinion, reason, nil +} + +func (r *RBACAuthorizer) RulesFor(ctx context.Context, user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { + var ( + resourceRules []authorizer.ResourceRuleInfo + nonResourceRules []authorizer.NonResourceRuleInfo + ) + + policyRules, err := r.authorizationRuleResolver.RulesFor(ctx, user, namespace) + for _, policyRule := range policyRules { + if len(policyRule.Resources) > 0 { + r := authorizer.DefaultResourceRuleInfo{ + Verbs: policyRule.Verbs, + APIGroups: policyRule.APIGroups, + Resources: policyRule.Resources, + ResourceNames: policyRule.ResourceNames, + } + var resourceRule authorizer.ResourceRuleInfo = &r + resourceRules = append(resourceRules, resourceRule) + } + if len(policyRule.NonResourceURLs) > 0 { + r := authorizer.DefaultNonResourceRuleInfo{ + Verbs: policyRule.Verbs, + NonResourceURLs: policyRule.NonResourceURLs, + } + var nonResourceRule authorizer.NonResourceRuleInfo = &r + nonResourceRules = append(nonResourceRules, nonResourceRule) + } + } + return resourceRules, nonResourceRules, false, err +} + +func New(roles rbacregistryvalidation.RoleGetter, roleBindings rbacregistryvalidation.RoleBindingLister, clusterRoles rbacregistryvalidation.ClusterRoleGetter, clusterRoleBindings rbacregistryvalidation.ClusterRoleBindingLister) *RBACAuthorizer { + authorizer := &RBACAuthorizer{ + authorizationRuleResolver: rbacregistryvalidation.NewDefaultRuleResolver( + roles, roleBindings, clusterRoles, clusterRoleBindings, + ), + } + return authorizer +} + +func RulesAllow(requestAttributes authorizer.Attributes, rules ...rbacv1.PolicyRule) bool { + for i := range rules { + if RuleAllows(requestAttributes, &rules[i]) { + return true + } + } + + return false +} + +func RuleAllows(requestAttributes authorizer.Attributes, rule *rbacv1.PolicyRule) bool { + if requestAttributes.IsResourceRequest() { + combinedResource := requestAttributes.GetResource() + if len(requestAttributes.GetSubresource()) > 0 { + combinedResource = requestAttributes.GetResource() + "/" + requestAttributes.GetSubresource() + } + + return rbacv1helpers.VerbMatches(rule, requestAttributes.GetVerb()) && + rbacv1helpers.APIGroupMatches(rule, requestAttributes.GetAPIGroup()) && + rbacv1helpers.ResourceMatches(rule, combinedResource, requestAttributes.GetSubresource()) && + rbacv1helpers.ResourceNameMatches(rule, requestAttributes.GetName()) + } + + return rbacv1helpers.VerbMatches(rule, requestAttributes.GetVerb()) && + rbacv1helpers.NonResourceURLMatches(rule, requestAttributes.GetPath()) +} + +type RoleGetter struct { + Lister rbaclisters.RoleLister +} + +func (g *RoleGetter) GetRole(ctx context.Context, namespace, name string) (*rbacv1.Role, error) { + return g.Lister.Roles(namespace).Get(name) +} + +type RoleBindingLister struct { + Lister rbaclisters.RoleBindingLister +} + +func (l *RoleBindingLister) ListRoleBindings(ctx context.Context, namespace string) ([]*rbacv1.RoleBinding, error) { + return l.Lister.RoleBindings(namespace).List(labels.Everything()) +} + +type ClusterRoleGetter struct { + Lister rbaclisters.ClusterRoleLister +} + +func (g *ClusterRoleGetter) GetClusterRole(ctx context.Context, name string) (*rbacv1.ClusterRole, error) { + return g.Lister.Get(name) +} + +type ClusterRoleBindingLister struct { + Lister rbaclisters.ClusterRoleBindingLister +} + +func (l *ClusterRoleBindingLister) ListClusterRoleBindings(ctx context.Context) ([]*rbacv1.ClusterRoleBinding, error) { + return l.Lister.List(labels.Everything()) +} diff --git a/internal/operator-controller/authorization/rbac.go b/internal/operator-controller/authorization/rbac.go new file mode 100644 index 000000000..7ec0bb0a6 --- /dev/null +++ b/internal/operator-controller/authorization/rbac.go @@ -0,0 +1,469 @@ +package authorization + +import ( + "context" + "errors" + "fmt" + "io" + "maps" + "sort" + + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + apimachyaml "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/request" + "sigs.k8s.io/controller-runtime/pkg/client" + + rbacinternal "github.com/operator-framework/operator-controller/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac" + rbacv1helpers "github.com/operator-framework/operator-controller/internal/operator-controller/authorization/internal/kubernetes/pkg/apis/rbac/v1" + rbacregistry "github.com/operator-framework/operator-controller/internal/operator-controller/authorization/internal/kubernetes/pkg/registry/rbac" + "github.com/operator-framework/operator-controller/internal/operator-controller/authorization/internal/kubernetes/pkg/registry/rbac/validation" + "github.com/operator-framework/operator-controller/internal/operator-controller/authorization/internal/kubernetes/plugin/pkg/auth/authorizer/rbac" +) + +type PreAuthorizer interface { + PreAuthorize(ctx context.Context, manifestManager user.Info, manifestReader io.Reader) (map[string][]rbacv1.PolicyRule, error) +} + +var ( + collectionVerbs = []string{"list", "watch", "create"} + objectVerbs = []string{"get", "patch", "update", "delete"} +) + +type rbacPreAuthorizer struct { + authorizer authorizer.Authorizer + ruleResolver validation.AuthorizationRuleResolver + restMapper meta.RESTMapper +} + +func NewRBACPreAuthorizer(cl client.Client) PreAuthorizer { + return &rbacPreAuthorizer{ + authorizer: newRBACAuthorizer(cl), + ruleResolver: newRBACRulesResolver(cl), + restMapper: cl.RESTMapper(), + } +} + +func (a *rbacPreAuthorizer) PreAuthorize(ctx context.Context, manifestManager user.Info, manifestReader io.Reader) (map[string][]rbacv1.PolicyRule, error) { + dm, err := a.decodeManifest(manifestReader) + if err != nil { + return nil, err + } + attributesRecords := dm.asAuthorizationAttributesRecordsForUser(manifestManager) + + var preAuthEvaluationErrors []error + missingRules, err := a.authorizeAttributesRecords(ctx, attributesRecords) + if err != nil { + preAuthEvaluationErrors = append(preAuthEvaluationErrors, err) + } + + ec := escalationChecker{ + authorizer: a.authorizer, + ruleResolver: a.ruleResolver, + extraClusterRoles: dm.clusterRoles, + extraRoles: dm.roles, + } + + for _, obj := range dm.rbacObjects() { + if err := ec.checkEscalation(ctx, manifestManager, obj); err != nil { + var peErr *validation.PrivilegeEscalationError + if errors.As(err, &peErr) { + missingRules[peErr.Namespace] = append(missingRules[peErr.Namespace], peErr.MissingRules...) + preAuthEvaluationErrors = append(preAuthEvaluationErrors, peErr.RuleResolutionErrors...) + } else { + preAuthEvaluationErrors = append(preAuthEvaluationErrors, err) + } + } + } + + for ns, nsMissingRules := range missingRules { + if compactMissingRules, err := validation.CompactRules(nsMissingRules); err == nil { + missingRules[ns] = compactMissingRules + } + sortableRules := rbacv1helpers.SortableRuleSlice(missingRules[ns]) + sort.Sort(sortableRules) + } + + if len(preAuthEvaluationErrors) > 0 { + return missingRules, fmt.Errorf("authorization evaluation errors: %w", errors.Join(preAuthEvaluationErrors...)) + } + return missingRules, nil +} + +func (a *rbacPreAuthorizer) decodeManifest(manifestReader io.Reader) (*decodedManifest, error) { + dm := &decodedManifest{ + gvrs: map[schema.GroupVersionResource][]types.NamespacedName{}, + clusterRoles: map[client.ObjectKey]rbacv1.ClusterRole{}, + roles: map[client.ObjectKey]rbacv1.Role{}, + clusterRoleBindings: map[client.ObjectKey]rbacv1.ClusterRoleBinding{}, + roleBindings: map[client.ObjectKey]rbacv1.RoleBinding{}, + } + var ( + i int + errs []error + decoder = apimachyaml.NewYAMLOrJSONDecoder(manifestReader, 1024) + ) + for { + var uObj unstructured.Unstructured + err := decoder.Decode(&uObj) + if errors.Is(err, io.EOF) { + break + } + if err != nil { + errs = append(errs, fmt.Errorf("could not decode object %d in manifest: %w", i, err)) + continue + } + gvk := uObj.GroupVersionKind() + restMapping, err := a.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + errs = append(errs, fmt.Errorf("could not get REST mapping for object %d in manifest with GVK %s: %w", i, gvk, err)) + continue + } + + gvr := restMapping.Resource + dm.gvrs[gvr] = append(dm.gvrs[gvr], client.ObjectKeyFromObject(&uObj)) + + switch restMapping.Resource.GroupResource() { + case schema.GroupResource{Group: rbacv1.GroupName, Resource: "clusterroles"}: + obj := &rbacv1.ClusterRole{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(uObj.UnstructuredContent(), obj); err != nil { + errs = append(errs, fmt.Errorf("could not decode object %d in manifest as ClusterRole: %w", i, err)) + continue + } + dm.clusterRoles[client.ObjectKeyFromObject(obj)] = *obj + case schema.GroupResource{Group: rbacv1.GroupName, Resource: "clusterrolebindings"}: + obj := &rbacv1.ClusterRoleBinding{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(uObj.UnstructuredContent(), obj); err != nil { + errs = append(errs, fmt.Errorf("could not decode object %d in manifest as ClusterRoleBinding: %w", i, err)) + continue + } + dm.clusterRoleBindings[client.ObjectKeyFromObject(obj)] = *obj + case schema.GroupResource{Group: rbacv1.GroupName, Resource: "roles"}: + obj := &rbacv1.Role{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(uObj.UnstructuredContent(), obj); err != nil { + errs = append(errs, fmt.Errorf("could not decode object %d in manifest as Role: %w", i, err)) + continue + } + dm.roles[client.ObjectKeyFromObject(obj)] = *obj + case schema.GroupResource{Group: rbacv1.GroupName, Resource: "rolebindings"}: + obj := &rbacv1.RoleBinding{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(uObj.UnstructuredContent(), obj); err != nil { + errs = append(errs, fmt.Errorf("could not decode object %d in manifest as RoleBinding: %w", i, err)) + continue + } + dm.roleBindings[client.ObjectKeyFromObject(obj)] = *obj + } + } + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + return dm, nil +} + +func (a *rbacPreAuthorizer) authorizeAttributesRecords(ctx context.Context, attributesRecords []authorizer.AttributesRecord) (map[string][]rbacv1.PolicyRule, error) { + var ( + missingRules = map[string][]rbacv1.PolicyRule{} + errs []error + ) + for _, ar := range attributesRecords { + allow, err := a.attributesAllowed(ctx, ar) + if err != nil { + errs = append(errs, err) + continue + } + if !allow { + missingRules[ar.Namespace] = append(missingRules[ar.Namespace], policyRuleFromAttributesRecord(ar)) + } + } + return missingRules, errors.Join(errs...) +} + +func (a *rbacPreAuthorizer) attributesAllowed(ctx context.Context, attributesRecord authorizer.AttributesRecord) (bool, error) { + decision, _, err := a.authorizer.Authorize(ctx, attributesRecord) + if err != nil { + return false, err + } + return decision == authorizer.DecisionAllow, nil +} + +func policyRuleFromAttributesRecord(attributesRecord authorizer.AttributesRecord) rbacv1.PolicyRule { + pr := rbacv1.PolicyRule{} + if attributesRecord.Verb != "" { + pr.Verbs = []string{attributesRecord.Verb} + } + if !attributesRecord.ResourceRequest { + pr.NonResourceURLs = []string{attributesRecord.Path} + return pr + } + + pr.APIGroups = []string{attributesRecord.APIGroup} + if attributesRecord.Name != "" { + pr.ResourceNames = []string{attributesRecord.Name} + } + + r := attributesRecord.Resource + if attributesRecord.Subresource != "" { + r += "/" + attributesRecord.Subresource + } + pr.Resources = []string{r} + + return pr +} + +type decodedManifest struct { + gvrs map[schema.GroupVersionResource][]types.NamespacedName + clusterRoles map[client.ObjectKey]rbacv1.ClusterRole + roles map[client.ObjectKey]rbacv1.Role + clusterRoleBindings map[client.ObjectKey]rbacv1.ClusterRoleBinding + roleBindings map[client.ObjectKey]rbacv1.RoleBinding +} + +func (dm *decodedManifest) rbacObjects() []client.Object { + objects := make([]client.Object, 0, len(dm.clusterRoles)+len(dm.roles)+len(dm.clusterRoleBindings)+len(dm.roleBindings)) + for obj := range maps.Values(dm.clusterRoles) { + objects = append(objects, &obj) + } + for obj := range maps.Values(dm.roles) { + objects = append(objects, &obj) + } + for obj := range maps.Values(dm.clusterRoleBindings) { + objects = append(objects, &obj) + } + for obj := range maps.Values(dm.roleBindings) { + objects = append(objects, &obj) + } + return objects +} + +func (dm *decodedManifest) asAuthorizationAttributesRecordsForUser(manifestManager user.Info) []authorizer.AttributesRecord { + var attributeRecords []authorizer.AttributesRecord + for gvr, keys := range dm.gvrs { + namespaces := sets.New[string]() + for _, k := range keys { + namespaces.Insert(k.Namespace) + for _, v := range objectVerbs { + attributeRecords = append(attributeRecords, authorizer.AttributesRecord{ + User: manifestManager, + Namespace: k.Namespace, + Name: k.Name, + APIGroup: gvr.Group, + APIVersion: gvr.Version, + Resource: gvr.Resource, + ResourceRequest: true, + Verb: v, + }) + } + } + for _, ns := range sets.List(namespaces) { + for _, v := range collectionVerbs { + attributeRecords = append(attributeRecords, authorizer.AttributesRecord{ + User: manifestManager, + Namespace: ns, + APIGroup: gvr.Group, + APIVersion: gvr.Version, + Resource: gvr.Resource, + ResourceRequest: true, + Verb: v, + }) + } + } + } + return attributeRecords +} + +func newRBACAuthorizer(cl client.Client) authorizer.Authorizer { + rg := &rbacGetter{cl: cl} + return rbac.New(rg, rg, rg, rg) +} + +type rbacGetter struct { + cl client.Client +} + +func (r rbacGetter) ListClusterRoleBindings(ctx context.Context) ([]*rbacv1.ClusterRoleBinding, error) { + var clusterRoleBindingsList rbacv1.ClusterRoleBindingList + if err := r.cl.List(ctx, &clusterRoleBindingsList); err != nil { + return nil, err + } + return toPtrSlice(clusterRoleBindingsList.Items), nil +} + +func (r rbacGetter) GetClusterRole(ctx context.Context, name string) (*rbacv1.ClusterRole, error) { + var clusterRole rbacv1.ClusterRole + if err := r.cl.Get(ctx, client.ObjectKey{Name: name}, &clusterRole); err != nil { + return nil, err + } + return &clusterRole, nil +} + +func (r rbacGetter) ListRoleBindings(ctx context.Context, namespace string) ([]*rbacv1.RoleBinding, error) { + var roleBindingsList rbacv1.RoleBindingList + if err := r.cl.List(ctx, &roleBindingsList, client.InNamespace(namespace)); err != nil { + return nil, err + } + return toPtrSlice(roleBindingsList.Items), nil +} + +func (r rbacGetter) GetRole(ctx context.Context, namespace, name string) (*rbacv1.Role, error) { + var role rbacv1.Role + if err := r.cl.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, &role); err != nil { + return nil, err + } + return &role, nil +} + +func newRBACRulesResolver(cl client.Client) validation.AuthorizationRuleResolver { + rg := &rbacGetter{cl: cl} + return validation.NewDefaultRuleResolver(rg, rg, rg, rg) +} + +type escalationChecker struct { + authorizer authorizer.Authorizer + ruleResolver validation.AuthorizationRuleResolver + extraRoles map[types.NamespacedName]rbacv1.Role + extraClusterRoles map[types.NamespacedName]rbacv1.ClusterRole +} + +func (ec *escalationChecker) checkEscalation(ctx context.Context, manifestManager user.Info, obj client.Object) error { + ctx = request.WithUser(request.WithNamespace(ctx, obj.GetNamespace()), manifestManager) + switch v := obj.(type) { + case *rbacv1.Role: + ctx = request.WithRequestInfo(ctx, &request.RequestInfo{APIGroup: rbacv1.GroupName, Resource: "roles", IsResourceRequest: true}) + return ec.checkRoleEscalation(ctx, v) + case *rbacv1.RoleBinding: + ctx = request.WithRequestInfo(ctx, &request.RequestInfo{APIGroup: rbacv1.GroupName, Resource: "rolebindings", IsResourceRequest: true}) + return ec.checkRoleBindingEscalation(ctx, v) + case *rbacv1.ClusterRole: + ctx = request.WithRequestInfo(ctx, &request.RequestInfo{APIGroup: rbacv1.GroupName, Resource: "clusterroles", IsResourceRequest: true}) + return ec.checkClusterRoleEscalation(ctx, v) + case *rbacv1.ClusterRoleBinding: + ctx = request.WithRequestInfo(ctx, &request.RequestInfo{APIGroup: rbacv1.GroupName, Resource: "clusterrolebindings", IsResourceRequest: true}) + return ec.checkClusterRoleBindingEscalation(ctx, v) + default: + return fmt.Errorf("unknown object type %T", v) + } +} + +func (ec *escalationChecker) checkClusterRoleEscalation(ctx context.Context, clusterRole *rbacv1.ClusterRole) error { + if rbacregistry.EscalationAllowed(ctx) || rbacregistry.RoleEscalationAuthorized(ctx, ec.authorizer) { + return nil + } + + // to set the aggregation rule, since it can gather anything, requires * on *.* + if hasAggregationRule(clusterRole) { + if err := validation.ConfirmNoEscalation(ctx, ec.ruleResolver, fullAuthority); err != nil { + return fmt.Errorf("must have cluster-admin privileges to use an aggregationRule: %w", err) + } + } + + if err := validation.ConfirmNoEscalation(ctx, ec.ruleResolver, clusterRole.Rules); err != nil { + return err + } + return nil +} + +func (ec *escalationChecker) checkClusterRoleBindingEscalation(ctx context.Context, clusterRoleBinding *rbacv1.ClusterRoleBinding) error { + if rbacregistry.EscalationAllowed(ctx) { + return nil + } + + roleRef := rbacinternal.RoleRef{} + err := rbacv1helpers.Convert_v1_RoleRef_To_rbac_RoleRef(&clusterRoleBinding.RoleRef, &roleRef, nil) + if err != nil { + return err + } + + if rbacregistry.BindingAuthorized(ctx, roleRef, metav1.NamespaceNone, ec.authorizer) { + return nil + } + + rules, err := ec.ruleResolver.GetRoleReferenceRules(ctx, clusterRoleBinding.RoleRef, metav1.NamespaceNone) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + + if clusterRoleBinding.RoleRef.Kind == "ClusterRole" { + if manifestClusterRole, ok := ec.extraClusterRoles[types.NamespacedName{Name: clusterRoleBinding.RoleRef.Name}]; ok { + rules = append(rules, manifestClusterRole.Rules...) + } + } + + if err := validation.ConfirmNoEscalation(ctx, ec.ruleResolver, rules); err != nil { + return err + } + return nil +} + +func (ec *escalationChecker) checkRoleEscalation(ctx context.Context, role *rbacv1.Role) error { + if rbacregistry.EscalationAllowed(ctx) || rbacregistry.RoleEscalationAuthorized(ctx, ec.authorizer) { + return nil + } + + rules := role.Rules + if err := validation.ConfirmNoEscalation(ctx, ec.ruleResolver, rules); err != nil { + return err + } + return nil +} + +func (ec *escalationChecker) checkRoleBindingEscalation(ctx context.Context, roleBinding *rbacv1.RoleBinding) error { + if rbacregistry.EscalationAllowed(ctx) { + return nil + } + + roleRef := rbacinternal.RoleRef{} + err := rbacv1helpers.Convert_v1_RoleRef_To_rbac_RoleRef(&roleBinding.RoleRef, &roleRef, nil) + if err != nil { + return err + } + if rbacregistry.BindingAuthorized(ctx, roleRef, roleBinding.Namespace, ec.authorizer) { + return nil + } + + rules, err := ec.ruleResolver.GetRoleReferenceRules(ctx, roleBinding.RoleRef, roleBinding.Namespace) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + + switch roleRef.Kind { + case "ClusterRole": + if manifestClusterRole, ok := ec.extraClusterRoles[types.NamespacedName{Name: roleBinding.RoleRef.Name}]; ok { + rules = append(rules, manifestClusterRole.Rules...) + } + case "Role": + if manifestRole, ok := ec.extraRoles[types.NamespacedName{Namespace: roleBinding.Namespace, Name: roleBinding.RoleRef.Name}]; ok { + rules = append(rules, manifestRole.Rules...) + } + } + + if err := validation.ConfirmNoEscalation(ctx, ec.ruleResolver, rules); err != nil { + return err + } + return nil +} + +var fullAuthority = []rbacv1.PolicyRule{ + {Verbs: []string{"*"}, APIGroups: []string{"*"}, Resources: []string{"*"}}, + {Verbs: []string{"*"}, NonResourceURLs: []string{"*"}}, +} + +func hasAggregationRule(clusterRole *rbacv1.ClusterRole) bool { + return clusterRole.AggregationRule != nil && len(clusterRole.AggregationRule.ClusterRoleSelectors) > 0 +} + +func toPtrSlice[V any](in []V) []*V { + out := make([]*V, len(in)) + for i := range in { + out[i] = &in[i] + } + return out +} diff --git a/internal/operator-controller/controllers/clusterextension_controller.go b/internal/operator-controller/controllers/clusterextension_controller.go index d914b831b..d2e52e760 100644 --- a/internal/operator-controller/controllers/clusterextension_controller.go +++ b/internal/operator-controller/controllers/clusterextension_controller.go @@ -96,6 +96,7 @@ type InstalledBundleGetter interface { //+kubebuilder:rbac:namespace=system,groups=core,resources=secrets,verbs=create;update;patch;delete;deletecollection;get;list;watch //+kubebuilder:rbac:groups=core,resources=serviceaccounts/token,verbs=create //+kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get +//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles;clusterrolebindings;roles;rolebindings,verbs=list;watch //+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clustercatalogs,verbs=list;watch