diff --git a/openshift-kube-apiserver/admission/admissionenablement/register.go b/openshift-kube-apiserver/admission/admissionenablement/register.go index bf08920de24af..9e530910eff2c 100644 --- a/openshift-kube-apiserver/admission/admissionenablement/register.go +++ b/openshift-kube-apiserver/admission/admissionenablement/register.go @@ -18,12 +18,14 @@ import ( "k8s.io/kubernetes/openshift-kube-apiserver/admission/network/externalipranger" "k8s.io/kubernetes/openshift-kube-apiserver/admission/network/restrictedendpoints" ingressadmission "k8s.io/kubernetes/openshift-kube-apiserver/admission/route" + "k8s.io/kubernetes/openshift-kube-apiserver/admission/route/hostassignment" projectnodeenv "k8s.io/kubernetes/openshift-kube-apiserver/admission/scheduler/nodeenv" schedulerpodnodeconstraints "k8s.io/kubernetes/openshift-kube-apiserver/admission/scheduler/podnodeconstraints" ) func RegisterOpenshiftKubeAdmissionPlugins(plugins *admission.Plugins) { authorizationrestrictusers.Register(plugins) + hostassignment.Register(plugins) imagepolicy.Register(plugins) ingressadmission.Register(plugins) managementcpusoverride.Register(plugins) @@ -65,6 +67,7 @@ var ( "security.openshift.io/SecurityContextConstraint", "security.openshift.io/SCCExecRestrictions", "route.openshift.io/IngressAdmission", + hostassignment.PluginName, // "route.openshift.io/RouteHostAssignment" } // openshiftAdmissionPluginsForKubeAfterResourceQuota are the plugins to add after ResourceQuota plugin diff --git a/openshift-kube-apiserver/admission/route/apis/hostassignment/doc.go b/openshift-kube-apiserver/admission/route/apis/hostassignment/doc.go new file mode 100644 index 0000000000000..1e09e2208b6a2 --- /dev/null +++ b/openshift-kube-apiserver/admission/route/apis/hostassignment/doc.go @@ -0,0 +1,4 @@ +// +k8s:deepcopy-gen=package,register + +// Package hostassignment is the internal version of the API. +package hostassignment diff --git a/openshift-kube-apiserver/admission/route/apis/hostassignment/register.go b/openshift-kube-apiserver/admission/route/apis/hostassignment/register.go new file mode 100644 index 0000000000000..d43ac830c6152 --- /dev/null +++ b/openshift-kube-apiserver/admission/route/apis/hostassignment/register.go @@ -0,0 +1,31 @@ +package hostassignment + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// SchemeGroupVersion is group version used to register these objects +var GroupVersion = schema.GroupVersion{Group: "route.openshift.io", Version: runtime.APIVersionInternal} + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return GroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns back a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return GroupVersion.WithResource(resource).GroupResource() +} + +var ( + schemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + Install = schemeBuilder.AddToScheme +) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(GroupVersion, + &HostAssignmentAdmissionConfig{}, + ) + return nil +} diff --git a/openshift-kube-apiserver/admission/route/apis/hostassignment/types.go b/openshift-kube-apiserver/admission/route/apis/hostassignment/types.go new file mode 100644 index 0000000000000..05b11cf541cac --- /dev/null +++ b/openshift-kube-apiserver/admission/route/apis/hostassignment/types.go @@ -0,0 +1,17 @@ +package hostassignment + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// HostAssignmentAdmissionConfig is the configuration for the the route host assignment plugin. +type HostAssignmentAdmissionConfig struct { + metav1.TypeMeta + + // domain is used to generate a default host name for a route when the + // route's host name is empty. The generated host name will follow this + // pattern: "..". + Domain string +} diff --git a/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/doc.go b/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/doc.go new file mode 100644 index 0000000000000..07ffba69df66e --- /dev/null +++ b/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/doc.go @@ -0,0 +1,5 @@ +// +k8s:deepcopy-gen=package,register +// +k8s:conversion-gen=k8s.io/kubernetes/openshift-kube-apiserver/admission/route/apis/hostassignment + +// Package v1 is the v1 version of the API. +package v1 diff --git a/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/register.go b/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/register.go new file mode 100644 index 0000000000000..4db9b98bd4f31 --- /dev/null +++ b/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/register.go @@ -0,0 +1,64 @@ +/* +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. +*/ + +package v1 + +import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GroupName specifies the group name used to register the objects. +const GroupName = "route.openshift.io" + +// GroupVersion specifies the group and the version used to register the objects. +var GroupVersion = v1.GroupVersion{Group: GroupName, Version: "v1"} + +// SchemeGroupVersion is group version used to register these objects +// Deprecated: use GroupVersion instead. +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 and AddToScheme will stay in k8s.io/kubernetes. + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + // Depreciated: use Install instead + AddToScheme = localSchemeBuilder.AddToScheme + Install = 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(addKnownTypes) +} + +// Adds the list of known types to Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &HostAssignmentAdmissionConfig{}, + ) + // AddToGroupVersion allows the serialization of client types like ListOptions. + v1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/types.go b/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/types.go new file mode 100644 index 0000000000000..0537567d18355 --- /dev/null +++ b/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/types.go @@ -0,0 +1,17 @@ +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// HostAssignmentAdmissionConfig is the configuration for the the route host assignment plugin. +type HostAssignmentAdmissionConfig struct { + metav1.TypeMeta `json:",inline"` + + // domain is used to generate a default host name for a route when the + // route's host name is empty. The generated host name will follow this + // pattern: "..". + Domain string `json:"domain"` +} diff --git a/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/zz_generated.conversion.go b/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/zz_generated.conversion.go new file mode 100644 index 0000000000000..fd998501e97fd --- /dev/null +++ b/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/zz_generated.conversion.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 conversion-gen. DO NOT EDIT. + +package v1 + +import ( + conversion "k8s.io/apimachinery/pkg/conversion" + runtime "k8s.io/apimachinery/pkg/runtime" + hostassignment "k8s.io/kubernetes/openshift-kube-apiserver/admission/route/apis/hostassignment" +) + +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((*HostAssignmentAdmissionConfig)(nil), (*hostassignment.HostAssignmentAdmissionConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_HostAssignmentAdmissionConfig_To_hostassignment_HostAssignmentAdmissionConfig(a.(*HostAssignmentAdmissionConfig), b.(*hostassignment.HostAssignmentAdmissionConfig), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*hostassignment.HostAssignmentAdmissionConfig)(nil), (*HostAssignmentAdmissionConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_hostassignment_HostAssignmentAdmissionConfig_To_v1_HostAssignmentAdmissionConfig(a.(*hostassignment.HostAssignmentAdmissionConfig), b.(*HostAssignmentAdmissionConfig), scope) + }); err != nil { + return err + } + return nil +} + +func autoConvert_v1_HostAssignmentAdmissionConfig_To_hostassignment_HostAssignmentAdmissionConfig(in *HostAssignmentAdmissionConfig, out *hostassignment.HostAssignmentAdmissionConfig, s conversion.Scope) error { + out.Domain = in.Domain + return nil +} + +// Convert_v1_HostAssignmentAdmissionConfig_To_hostassignment_HostAssignmentAdmissionConfig is an autogenerated conversion function. +func Convert_v1_HostAssignmentAdmissionConfig_To_hostassignment_HostAssignmentAdmissionConfig(in *HostAssignmentAdmissionConfig, out *hostassignment.HostAssignmentAdmissionConfig, s conversion.Scope) error { + return autoConvert_v1_HostAssignmentAdmissionConfig_To_hostassignment_HostAssignmentAdmissionConfig(in, out, s) +} + +func autoConvert_hostassignment_HostAssignmentAdmissionConfig_To_v1_HostAssignmentAdmissionConfig(in *hostassignment.HostAssignmentAdmissionConfig, out *HostAssignmentAdmissionConfig, s conversion.Scope) error { + out.Domain = in.Domain + return nil +} + +// Convert_hostassignment_HostAssignmentAdmissionConfig_To_v1_HostAssignmentAdmissionConfig is an autogenerated conversion function. +func Convert_hostassignment_HostAssignmentAdmissionConfig_To_v1_HostAssignmentAdmissionConfig(in *hostassignment.HostAssignmentAdmissionConfig, out *HostAssignmentAdmissionConfig, s conversion.Scope) error { + return autoConvert_hostassignment_HostAssignmentAdmissionConfig_To_v1_HostAssignmentAdmissionConfig(in, out, s) +} diff --git a/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/zz_generated.deepcopy.go b/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/zz_generated.deepcopy.go new file mode 100644 index 0000000000000..ee31de89c12e9 --- /dev/null +++ b/openshift-kube-apiserver/admission/route/apis/hostassignment/v1/zz_generated.deepcopy.go @@ -0,0 +1,51 @@ +//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 v1 + +import ( + 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 *HostAssignmentAdmissionConfig) DeepCopyInto(out *HostAssignmentAdmissionConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAssignmentAdmissionConfig. +func (in *HostAssignmentAdmissionConfig) DeepCopy() *HostAssignmentAdmissionConfig { + if in == nil { + return nil + } + out := new(HostAssignmentAdmissionConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HostAssignmentAdmissionConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/openshift-kube-apiserver/admission/route/apis/hostassignment/zz_generated.deepcopy.go b/openshift-kube-apiserver/admission/route/apis/hostassignment/zz_generated.deepcopy.go new file mode 100644 index 0000000000000..e24ccfb9a4e3e --- /dev/null +++ b/openshift-kube-apiserver/admission/route/apis/hostassignment/zz_generated.deepcopy.go @@ -0,0 +1,51 @@ +//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 hostassignment + +import ( + 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 *HostAssignmentAdmissionConfig) DeepCopyInto(out *HostAssignmentAdmissionConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAssignmentAdmissionConfig. +func (in *HostAssignmentAdmissionConfig) DeepCopy() *HostAssignmentAdmissionConfig { + if in == nil { + return nil + } + out := new(HostAssignmentAdmissionConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HostAssignmentAdmissionConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/openshift-kube-apiserver/admission/route/hostassignment/admission.go b/openshift-kube-apiserver/admission/route/hostassignment/admission.go new file mode 100644 index 0000000000000..b7f8d084cb499 --- /dev/null +++ b/openshift-kube-apiserver/admission/route/hostassignment/admission.go @@ -0,0 +1,157 @@ +package hostassignment + +import ( + "context" + "fmt" + "io" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/initializer" + "k8s.io/client-go/kubernetes" + authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1" + + routev1 "github.com/openshift/api/route/v1" + "github.com/openshift/library-go/pkg/config/helpers" + "github.com/openshift/library-go/pkg/route/hostassignment" + hostassignmentapi "k8s.io/kubernetes/openshift-kube-apiserver/admission/route/apis/hostassignment" + hostassignmentv1 "k8s.io/kubernetes/openshift-kube-apiserver/admission/route/apis/hostassignment/v1" +) + +const PluginName = "route.openshift.io/RouteHostAssignment" + +func Register(plugins *admission.Plugins) { + plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { + pluginConfig, err := readConfig(config) + if err != nil { + return nil, err + } + return newHostAssignment(pluginConfig) + }) +} + +type hostAssignment struct { + *admission.Handler + + hostnameGenerator hostassignment.HostnameGenerator + sarClient authorizationv1.SubjectAccessReviewInterface +} + +func readConfig(reader io.Reader) (*hostassignmentapi.HostAssignmentAdmissionConfig, error) { + obj, err := helpers.ReadYAMLToInternal(reader, hostassignmentapi.Install, hostassignmentv1.Install) + if err != nil { + return nil, err + } + if obj == nil { + scheme := runtime.NewScheme() + hostassignmentapi.Install(scheme) + hostassignmentv1.Install(scheme) + external := &hostassignmentv1.HostAssignmentAdmissionConfig{} + scheme.Default(external) + internal := &hostassignmentapi.HostAssignmentAdmissionConfig{} + if err := scheme.Convert(external, internal, nil); err != nil { + return nil, fmt.Errorf("failed to produce default config: %w", err) + } + obj = internal + } + config, ok := obj.(*hostassignmentapi.HostAssignmentAdmissionConfig) + if !ok { + return nil, fmt.Errorf("unexpected config object: %#v", obj) + } + return config, nil +} + +func newHostAssignment(config *hostassignmentapi.HostAssignmentAdmissionConfig) (*hostAssignment, error) { + hostnameGenerator, err := hostassignment.NewSimpleAllocationPlugin(config.Domain) + if err != nil { + return nil, fmt.Errorf("configuration failed: %w", err) + } + return &hostAssignment{ + Handler: admission.NewHandler(admission.Create, admission.Update), + hostnameGenerator: hostnameGenerator, + }, nil +} + +func toRoute(uncastObj runtime.Object) (*routev1.Route, runtime.Unstructured, field.ErrorList) { + u, ok := uncastObj.(runtime.Unstructured) + if !ok { + return nil, nil, field.ErrorList{ + field.NotSupported(field.NewPath("kind"), fmt.Sprintf("%T", uncastObj), []string{"Route"}), + field.NotSupported(field.NewPath("apiVersion"), fmt.Sprintf("%T", uncastObj), []string{routev1.GroupVersion.String()}), + } + } + + var out routev1.Route + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &out); err != nil { + return nil, nil, field.ErrorList{ + field.NotSupported(field.NewPath("kind"), fmt.Sprintf("%T", uncastObj), []string{"Route"}), + field.NotSupported(field.NewPath("apiVersion"), fmt.Sprintf("%T", uncastObj), []string{routev1.GroupVersion.String()}), + } + } + + return &out, u, nil +} + +var _ admission.MutationInterface = &hostAssignment{} + +func (a *hostAssignment) Admit(ctx context.Context, attributes admission.Attributes, o admission.ObjectInterfaces) error { + if attributes.GetResource().GroupResource() != (schema.GroupResource{Group: "route.openshift.io", Resource: "routes"}) { + return nil + } + // if a subresource is specified, skip it + if len(attributes.GetSubresource()) > 0 { + return nil + } + + switch attributes.GetOperation() { + case admission.Create: + r, u, errs := toRoute(attributes.GetObject()) + if len(errs) > 0 { + return errors.NewInvalid(attributes.GetKind().GroupKind(), attributes.GetName(), errs) + } + errs = hostassignment.AllocateHost(ctx, r, a.sarClient, a.hostnameGenerator) + if len(errs) > 0 { + return errors.NewInvalid(attributes.GetKind().GroupKind(), attributes.GetName(), errs) + } + content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(r) + if err != nil { + return errors.NewInvalid(attributes.GetKind().GroupKind(), attributes.GetName(), field.ErrorList{ + field.InternalError(field.NewPath(""), err), + }) + } + u.SetUnstructuredContent(content) + case admission.Update: + r, _, errs := toRoute(attributes.GetObject()) + if len(errs) > 0 { + return errors.NewInvalid(attributes.GetKind().GroupKind(), attributes.GetName(), errs) + } + old, _, errs := toRoute(attributes.GetOldObject()) + if len(errs) > 0 { + return errors.NewInvalid(attributes.GetKind().GroupKind(), attributes.GetName(), errs) + } + errs = hostassignment.ValidateHostUpdate(ctx, r, old, a.sarClient) + if len(errs) > 0 { + return errors.NewInvalid(attributes.GetKind().GroupKind(), attributes.GetName(), errs) + } + default: + return admission.NewForbidden(attributes, fmt.Errorf("unhandled operation: %v", attributes.GetOperation())) + } + + return nil +} + +var _ initializer.WantsExternalKubeClientSet = &hostAssignment{} + +func (a *hostAssignment) SetExternalKubeClientSet(clientset kubernetes.Interface) { + a.sarClient = clientset.AuthorizationV1().SubjectAccessReviews() +} + +func (a *hostAssignment) ValidateInitialization() error { + if a.sarClient == nil { + return fmt.Errorf("missing SubjectAccessReview client") + } + return nil +} diff --git a/vendor/github.com/openshift/library-go/pkg/authorization/authorizationutil/subject.go b/vendor/github.com/openshift/library-go/pkg/authorization/authorizationutil/subject.go new file mode 100644 index 0000000000000..74c179e686556 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/authorization/authorizationutil/subject.go @@ -0,0 +1,56 @@ +package authorizationutil + +import ( + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apiserver/pkg/authentication/serviceaccount" +) + +func BuildRBACSubjects(users, groups []string) []rbacv1.Subject { + subjects := []rbacv1.Subject{} + + for _, user := range users { + saNamespace, saName, err := serviceaccount.SplitUsername(user) + if err == nil { + subjects = append(subjects, rbacv1.Subject{Kind: rbacv1.ServiceAccountKind, Namespace: saNamespace, Name: saName}) + } else { + subjects = append(subjects, rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: user}) + } + } + + for _, group := range groups { + subjects = append(subjects, rbacv1.Subject{Kind: rbacv1.GroupKind, APIGroup: rbacv1.GroupName, Name: group}) + } + + return subjects +} + +func RBACSubjectsToUsersAndGroups(subjects []rbacv1.Subject, defaultNamespace string) (users []string, groups []string) { + for _, subject := range subjects { + + switch { + case subject.APIGroup == rbacv1.GroupName && subject.Kind == rbacv1.GroupKind: + groups = append(groups, subject.Name) + case subject.APIGroup == rbacv1.GroupName && subject.Kind == rbacv1.UserKind: + users = append(users, subject.Name) + case subject.APIGroup == "" && subject.Kind == rbacv1.ServiceAccountKind: + // default the namespace to namespace we're working in if + // it's available. This allows rolebindings that reference + // SAs in the local namespace to avoid having to qualify + // them. + ns := defaultNamespace + if len(subject.Namespace) > 0 { + ns = subject.Namespace + } + if len(ns) > 0 { + name := serviceaccount.MakeUsername(ns, subject.Name) + users = append(users, name) + } else { + // maybe error? this fails safe at any rate + } + default: + // maybe error? This fails safe at any rate + } + } + + return users, groups +} diff --git a/vendor/github.com/openshift/library-go/pkg/authorization/authorizationutil/util.go b/vendor/github.com/openshift/library-go/pkg/authorization/authorizationutil/util.go new file mode 100644 index 0000000000000..040d0f643485c --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/authorization/authorizationutil/util.go @@ -0,0 +1,50 @@ +package authorizationutil + +import ( + "context" + "errors" + + authorizationv1 "k8s.io/api/authorization/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authentication/user" + authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1" +) + +// AddUserToSAR adds the requisite user information to a SubjectAccessReview. +// It returns the modified SubjectAccessReview. +func AddUserToSAR(user user.Info, sar *authorizationv1.SubjectAccessReview) *authorizationv1.SubjectAccessReview { + sar.Spec.User = user.GetName() + // reminiscent of the bad old days of C. Copies copy the min number of elements of both source and dest + sar.Spec.Groups = make([]string, len(user.GetGroups())) + copy(sar.Spec.Groups, user.GetGroups()) + sar.Spec.Extra = map[string]authorizationv1.ExtraValue{} + + for k, v := range user.GetExtra() { + sar.Spec.Extra[k] = authorizationv1.ExtraValue(v) + } + + return sar +} + +// Authorize verifies that a given user is permitted to carry out a given +// action. If this cannot be determined, or if the user is not permitted, an +// error is returned. +func Authorize(sarClient authorizationclient.SubjectAccessReviewInterface, user user.Info, resourceAttributes *authorizationv1.ResourceAttributes) error { + sar := AddUserToSAR(user, &authorizationv1.SubjectAccessReview{ + Spec: authorizationv1.SubjectAccessReviewSpec{ + ResourceAttributes: resourceAttributes, + }, + }) + + resp, err := sarClient.Create(context.TODO(), sar, metav1.CreateOptions{}) + if err == nil && resp != nil && resp.Status.Allowed { + return nil + } + + if err == nil { + err = errors.New(resp.Status.Reason) + } + return kerrors.NewForbidden(schema.GroupResource{Group: resourceAttributes.Group, Resource: resourceAttributes.Resource}, resourceAttributes.Name, err) +} diff --git a/vendor/github.com/openshift/library-go/pkg/route/hostassignment/assignment.go b/vendor/github.com/openshift/library-go/pkg/route/hostassignment/assignment.go new file mode 100644 index 0000000000000..bdf9e0e7800ce --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/route/hostassignment/assignment.go @@ -0,0 +1,197 @@ +package hostassignment + +import ( + "context" + "fmt" + + authorizationv1 "k8s.io/api/authorization/v1" + apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/endpoints/request" + + routev1 "github.com/openshift/api/route/v1" + "github.com/openshift/library-go/pkg/authorization/authorizationutil" +) + +// HostGeneratedAnnotationKey is the key for an annotation set to "true" if the route's host was generated +const HostGeneratedAnnotationKey = "openshift.io/host.generated" + +// Registry is an interface for performing subject access reviews +type SubjectAccessReviewCreator interface { + Create(ctx context.Context, sar *authorizationv1.SubjectAccessReview, opts metav1.CreateOptions) (*authorizationv1.SubjectAccessReview, error) +} + +type HostnameGenerator interface { + GenerateHostname(*routev1.Route) (string, error) +} + +// AllocateHost allocates a host name ONLY if the route doesn't specify a subdomain wildcard policy and +// the host name on the route is empty and an allocator is configured. +// It must first allocate the shard and may return an error if shard allocation fails. +func AllocateHost(ctx context.Context, route *routev1.Route, sarc SubjectAccessReviewCreator, routeAllocator HostnameGenerator) field.ErrorList { + hostSet := len(route.Spec.Host) > 0 + certSet := route.Spec.TLS != nil && (len(route.Spec.TLS.CACertificate) > 0 || len(route.Spec.TLS.Certificate) > 0 || len(route.Spec.TLS.DestinationCACertificate) > 0 || len(route.Spec.TLS.Key) > 0) + if hostSet || certSet { + user, ok := request.UserFrom(ctx) + if !ok { + return field.ErrorList{field.InternalError(field.NewPath("spec", "host"), fmt.Errorf("unable to verify host field can be set"))} + } + res, err := sarc.Create( + ctx, + authorizationutil.AddUserToSAR( + user, + &authorizationv1.SubjectAccessReview{ + Spec: authorizationv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Namespace: request.NamespaceValue(ctx), + Verb: "create", + Group: routev1.GroupName, + Resource: "routes", + Subresource: "custom-host", + }, + }, + }, + ), + metav1.CreateOptions{}, + ) + if err != nil { + return field.ErrorList{field.InternalError(field.NewPath("spec", "host"), err)} + } + if !res.Status.Allowed { + if hostSet { + return field.ErrorList{field.Forbidden(field.NewPath("spec", "host"), "you do not have permission to set the host field of the route")} + } + return field.ErrorList{field.Forbidden(field.NewPath("spec", "tls"), "you do not have permission to set certificate fields on the route")} + } + } + + if route.Spec.WildcardPolicy == routev1.WildcardPolicySubdomain { + // Don't allocate a host if subdomain wildcard policy. + return nil + } + + if len(route.Spec.Subdomain) == 0 && len(route.Spec.Host) == 0 && routeAllocator != nil { + // TODO: this does not belong here, and should be removed + host, err := routeAllocator.GenerateHostname(route) + if err != nil { + return field.ErrorList{field.InternalError(field.NewPath("spec", "host"), fmt.Errorf("allocation error: %v for route: %#v", err, route))} + } + route.Spec.Host = host + if route.Annotations == nil { + route.Annotations = map[string]string{} + } + route.Annotations[HostGeneratedAnnotationKey] = "true" + } + return nil +} + +func hasCertificateInfo(tls *routev1.TLSConfig) bool { + if tls == nil { + return false + } + return len(tls.Certificate) > 0 || + len(tls.Key) > 0 || + len(tls.CACertificate) > 0 || + len(tls.DestinationCACertificate) > 0 +} + +func certificateChangeRequiresAuth(route, older *routev1.Route) bool { + switch { + case route.Spec.TLS != nil && older.Spec.TLS != nil: + a, b := route.Spec.TLS, older.Spec.TLS + if !hasCertificateInfo(a) { + // removing certificate info is allowed + return false + } + return a.CACertificate != b.CACertificate || + a.Certificate != b.Certificate || + a.DestinationCACertificate != b.DestinationCACertificate || + a.Key != b.Key + case route.Spec.TLS != nil: + // using any default certificate is allowed + return hasCertificateInfo(route.Spec.TLS) + default: + // all other cases we are not adding additional certificate info + return false + } +} + +func ValidateHostUpdate(ctx context.Context, route, older *routev1.Route, sarc SubjectAccessReviewCreator) field.ErrorList { + hostChanged := route.Spec.Host != older.Spec.Host + subdomainChanged := route.Spec.Subdomain != older.Spec.Subdomain + certChanged := certificateChangeRequiresAuth(route, older) + if !hostChanged && !certChanged && !subdomainChanged { + return nil + } + user, ok := request.UserFrom(ctx) + if !ok { + return field.ErrorList{field.InternalError(field.NewPath("spec", "host"), fmt.Errorf("unable to verify host field can be changed"))} + } + res, err := sarc.Create( + ctx, + authorizationutil.AddUserToSAR( + user, + &authorizationv1.SubjectAccessReview{ + Spec: authorizationv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Namespace: request.NamespaceValue(ctx), + Verb: "update", + Group: routev1.GroupName, + Resource: "routes", + Subresource: "custom-host", + }, + }, + }, + ), + metav1.CreateOptions{}, + ) + if err != nil { + if subdomainChanged { + return field.ErrorList{field.InternalError(field.NewPath("spec", "subdomain"), err)} + } + return field.ErrorList{field.InternalError(field.NewPath("spec", "host"), err)} + } + if !res.Status.Allowed { + if hostChanged { + return apimachineryvalidation.ValidateImmutableField(route.Spec.Host, older.Spec.Host, field.NewPath("spec", "host")) + } + if subdomainChanged { + return apimachineryvalidation.ValidateImmutableField(route.Spec.Subdomain, older.Spec.Subdomain, field.NewPath("spec", "subdomain")) + } + + // if tls is being updated without host being updated, we check if 'create' permission exists on custom-host subresource + res, err := sarc.Create( + ctx, + authorizationutil.AddUserToSAR( + user, + &authorizationv1.SubjectAccessReview{ + Spec: authorizationv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Namespace: request.NamespaceValue(ctx), + Verb: "create", + Group: routev1.GroupName, + Resource: "routes", + Subresource: "custom-host", + }, + }, + }, + ), + metav1.CreateOptions{}, + ) + if err != nil { + return field.ErrorList{field.InternalError(field.NewPath("spec", "host"), err)} + } + if !res.Status.Allowed { + if route.Spec.TLS == nil || older.Spec.TLS == nil { + return apimachineryvalidation.ValidateImmutableField(route.Spec.TLS, older.Spec.TLS, field.NewPath("spec", "tls")) + } + errs := apimachineryvalidation.ValidateImmutableField(route.Spec.TLS.CACertificate, older.Spec.TLS.CACertificate, field.NewPath("spec", "tls", "caCertificate")) + errs = append(errs, apimachineryvalidation.ValidateImmutableField(route.Spec.TLS.Certificate, older.Spec.TLS.Certificate, field.NewPath("spec", "tls", "certificate"))...) + errs = append(errs, apimachineryvalidation.ValidateImmutableField(route.Spec.TLS.DestinationCACertificate, older.Spec.TLS.DestinationCACertificate, field.NewPath("spec", "tls", "destinationCACertificate"))...) + errs = append(errs, apimachineryvalidation.ValidateImmutableField(route.Spec.TLS.Key, older.Spec.TLS.Key, field.NewPath("spec", "tls", "key"))...) + return errs + } + } + return nil +} diff --git a/vendor/github.com/openshift/library-go/pkg/route/hostassignment/plugin.go b/vendor/github.com/openshift/library-go/pkg/route/hostassignment/plugin.go new file mode 100644 index 0000000000000..e936bc66c4871 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/route/hostassignment/plugin.go @@ -0,0 +1,46 @@ +package hostassignment + +import ( + "fmt" + "strings" + + kvalidation "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/klog/v2" + + routev1 "github.com/openshift/api/route/v1" +) + +// Default DNS suffix to use if no configuration is passed to this plugin. +const defaultDNSSuffix = "router.default.svc.cluster.local" + +// SimpleAllocationPlugin implements the route.AllocationPlugin interface +// to provide a simple unsharded (or single sharded) allocation plugin. +type SimpleAllocationPlugin struct { + DNSSuffix string +} + +// NewSimpleAllocationPlugin creates a new SimpleAllocationPlugin. +func NewSimpleAllocationPlugin(suffix string) (*SimpleAllocationPlugin, error) { + if len(suffix) == 0 { + suffix = defaultDNSSuffix + } + + klog.V(4).Infof("Route plugin initialized with suffix=%s", suffix) + + // Check that the DNS suffix is valid. + if len(kvalidation.IsDNS1123Subdomain(suffix)) != 0 { + return nil, fmt.Errorf("invalid DNS suffix: %s", suffix) + } + + return &SimpleAllocationPlugin{DNSSuffix: suffix}, nil +} + +// GenerateHostname generates a host name for a route - using the service name, +// namespace (if provided) and the router shard dns suffix. +// TODO: move to router code, and have the routers set this back on the route status. +func (p *SimpleAllocationPlugin) GenerateHostname(route *routev1.Route) (string, error) { + if len(route.Name) == 0 || len(route.Namespace) == 0 { + return "", nil + } + return fmt.Sprintf("%s-%s.%s", strings.Replace(route.Name, ".", "-", -1), route.Namespace, p.DNSSuffix), nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 08fab79d61510..eeb3f4b889ec8 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -899,6 +899,7 @@ github.com/openshift/library-go/pkg/apiserver/admission/admissionregistrationtes github.com/openshift/library-go/pkg/apiserver/admission/admissionrestconfig github.com/openshift/library-go/pkg/apiserver/admission/admissiontimeout github.com/openshift/library-go/pkg/apiserver/apiserverconfig +github.com/openshift/library-go/pkg/authorization/authorizationutil github.com/openshift/library-go/pkg/authorization/hardcodedauthorizer github.com/openshift/library-go/pkg/authorization/scopemetadata github.com/openshift/library-go/pkg/client/openshiftrestmapper @@ -916,6 +917,7 @@ github.com/openshift/library-go/pkg/network github.com/openshift/library-go/pkg/oauth/oauthdiscovery github.com/openshift/library-go/pkg/quota/clusterquotamapping github.com/openshift/library-go/pkg/quota/quotautil +github.com/openshift/library-go/pkg/route/hostassignment github.com/openshift/library-go/pkg/route/validation github.com/openshift/library-go/pkg/security/ldaputil github.com/openshift/library-go/pkg/security/uid