diff --git a/Dockerfile.dapper b/Dockerfile.dapper index f257782a..20bb11d6 100644 --- a/Dockerfile.dapper +++ b/Dockerfile.dapper @@ -5,9 +5,9 @@ ENV ARCH $DAPPER_HOST_ARCH RUN zypper -n install git docker vim less file curl wget awk RUN if [ "${ARCH}" = "amd64" ]; then \ - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.50.1; \ + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.52.2; \ fi -ENV HELM_VERSION v3.11.3 +ENV HELM_VERSION v3.12.1 RUN curl -sL https://get.helm.sh/helm-${HELM_VERSION}-linux-${ARCH}.tar.gz | tar xvzf - -C /usr/local/bin --strip-components=1 RUN GOBIN=/usr/local/bin go install github.com/golang/mock/mockgen@v1.6.0 diff --git a/README.md b/README.md index bc7572fb..f2aec9d5 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,14 @@ is also rejected. The Go web-server itself is configured by [dynamiclistener](https://github.com/rancher/dynamiclistener). It handles TLS certificates and the management of associated Secrets for secure communication of other Rancher components with the Webhook. +## Docs + +Documentation on each of the resources that are validated or mutated can be found in `docs.md`. It is recommended to review the [kubernetes docs on CRDs](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#customresourcedefinitions) as well. + +Docs are added by creating a resource-specific readme in the directory of your mutator/validator (e.x. `pkg/resources/$GROUP/$GROUP_VERSION/$RESOURCE/$READABLE_RESOURCE.MD`). +These files should be named with a human-readable version of the resource's name. +Running `go generate` will then aggregate these into the user-facing docs in the `docs.md` file. + ## Webhooks Rancher-Webhook is composed of multiple [WebhookHandlers](pkg/admission/admission.go) which is used when creating [ValidatingWebhooks](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#validatingwebhook-v1-admissionregistration-k8s-io) and [MutatingWebhooks](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#mutatingwebhook-v1-admissionregistration-k8s-io). diff --git a/docs.md b/docs.md new file mode 100644 index 00000000..40c3cfa8 --- /dev/null +++ b/docs.md @@ -0,0 +1,129 @@ +# core/v1 + +## Namespace + +### Validation Checks + +Note: The kube-system namespace, unlike other namespaces, has a "failPolicy" of "ignore" on update calls. + +#### PSA Label Validation + +Validates that users who create or edit a PSA enforcement label on a namespace have the `updatepsa` verb on `projects` in `management.cattle.io/v3`. See the [upstream docs](https://kubernetes.io/docs/concepts/security/pod-security-admission/) for more information on the effect of these labels. + +The following labels are considered relevant labels for PSA enforcement: `"pod-security.kubernetes.io/enforce", "pod-security.kubernetes.io/enforce-version", "pod-security.kubernetes.io/audit", "pod-security.kubernetes.io/audit-version", "pod-security.kubernetes.io/warn", "pod-security.kubernetes.io/warn-version"`. + +# management.cattle.io/v3 + +## ClusterRoleTemplateBinding + +### Validation Checks + +#### Escalation Prevention + +Users can only create/update ClusterRoleTemplateBindings which grant permissions to RoleTemplates with rights less than or equal to those they currently possess. This is to prevent privilege escalation. + +#### Invalid Fields - Create + +Users cannot create ClusterRoleTemplateBindings which violate the following constraints: +- Either a user subject (through "UserName" or "UserPrincipalName") or a group subject (through "GroupName" or "GroupPrincipalName") must be specified; both a user subject and group subject cannot be specified +- A "ClusterName" must be specified +- The roleTemplate indicated in "RoleTemplateName" must be: + - Valid (i.e. is an existing `roleTemplate` object in the `management.cattle.io/v3` apiGroup) + - Not locked (i.e. `roleTemplate.Locked` must be `false`) + +#### Invalid Fields - Update + +Users cannot update the following fields after creation: +- RoleTemplateName +- ClusterName + +Users can update the following fields if they have not been set, but after they have been set they cannot be changed: +- UserName +- UserPrincipalName +- GroupName +- GroupPrincipalName + +In addition, as in the create validation, both a user subject and a group subject cannot be specified. + +## GlobalRole + +### Validation Checks + +Note: all checks are bypassed if the GlobalRole is being deleted + +#### Escalation Prevention + +Users can only change GlobalRoles with rights less than or equal to those they currently possess. This is to prevent privilege escalation. + +## GlobalRoleBinding + +### Validation Checks + +Note: all checks are bypassed if the GlobalRoleBinding is being deleted + +#### Escalation Prevention + +Users can only create/update GlobalRoleBindings with rights less than or equal to those they currently possess. This is to prevent privilege escalation. + +#### Valid Global Role Reference + +GlobalRoleBindings must refer to a valid global role (i.e. an existing `GlobalRole` object in the `management.cattle.io/v3` apiGroup). + +## NodeDriver + +### Validation Checks + +Note: checks only run if a node driver is being disabled or deleted + +#### Machine Deletion Prevention + +This admission webhook prevents the disabling or deletion of a NodeDriver if there are any Nodes that are under management by said driver. If there are _any_ nodes that use the driver the request will be denied. + +## ProjectRoleTemplateBinding + +### Validation Checks + +#### Escalation Prevention + +Users can only create/update ProjectRoleTemplateBindings with rights less than or equal to those they currently possess. This is to prevent privilege escalation. + +#### Invalid Fields - Create + +Users cannot create ProjectRoleTemplateBindings which violate the following constraints: +- Either a user subject (through "UserName" or "UserPrincipalName") or a group subject (through "GroupName" or "GroupPrincipalName") must be specified; both a user subject and group subject cannot be specified +- A "ProjectName" must be specified +- The roleTemplate indicated in "RoleTemplateName" must be: + - Valid (i.e. is an existing `roleTemplate` object in the `management.cattle.io/v3` apiGroup) + - Not locked (i.e. `roleTemplate.Locked` must be `false`) + +#### Invalid Fields - Update + +Users cannot update the following fields after creation: +- RoleTemplateName +- ProjectName + +Users can update the following fields if they have not been set, but after they have been set they cannot be changed: +- UserName +- UserPrincipalName +- GroupName +- GroupPrincipalName + +In addition, as in the create validation, both a user subject and a group subject cannot be specified. + +## RoleTemplate + +### Validation Checks + +Note: all checks are bypassed if the RoleTemplate is being deleted + +#### Circular Reference + +Circular references to webhooks (a inherits b, b inherits a) are not allowed. More specifically, if "roleTemplate1" is included in the `roleTemplateNames` of "roleTemplate2", then "roleTemplate2" must not be included in the `roleTemplateNames` of "roleTemplate1". This checks prevents the creation of roles whose end-state cannot be resolved. + +#### Rules Without Verbs + +Rules without verbs are not permitted. The `rules` included in a roleTemplate are of the same type as the rules used by standard kubernetes RBAC types (such as `Roles` from `rbac.authorization.k8s.io/v1`). Because of this, they inherit the same restrictions as these types, including this one. + +#### Escalation Prevention + +Users can only change RoleTemplates with rights less than or equal to those they currently possess. This prevents privilege escalation. diff --git a/go.mod b/go.mod index 4f190f37..49f4c603 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.2 golang.org/x/exp v0.0.0-20230206171751-46f607a40771 + golang.org/x/text v0.8.0 golang.org/x/tools v0.7.0 k8s.io/api v0.25.5 k8s.io/apiextensions-apiserver v0.25.5 @@ -122,7 +123,6 @@ require ( golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.6.0 // indirect golang.org/x/term v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/main.go b/main.go index 5b90bbdc..d9a1c5f5 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,5 @@ //go:generate go run pkg/codegen/cleanup/main.go -//go:generate go run pkg/codegen/main.go pkg/codegen/template.go +//go:generate go run ./pkg/codegen package main import ( diff --git a/package/Dockerfile b/package/Dockerfile index da207a13..c696e4e9 100644 --- a/package/Dockerfile +++ b/package/Dockerfile @@ -1,4 +1,4 @@ -FROM registry.suse.com/bci/bci-micro:15.4.19.2 +FROM registry.suse.com/bci/bci-micro:15.4.20.1 ARG user=webhook diff --git a/pkg/admission/admission_test.go b/pkg/admission/admission_test.go index 5b128046..7cc2185c 100644 --- a/pkg/admission/admission_test.go +++ b/pkg/admission/admission_test.go @@ -371,7 +371,7 @@ func (f *fakeValidatingAdmissionHandler) Operations() []v1.OperationType { return f.operations } -func (f *fakeValidatingAdmissionHandler) ValidatingWebhook(clientConfig v1.WebhookClientConfig) []v1.ValidatingWebhook { +func (f *fakeValidatingAdmissionHandler) ValidatingWebhook(_ v1.WebhookClientConfig) []v1.ValidatingWebhook { return nil } @@ -401,7 +401,7 @@ func (f *fakeMutatingAdmissionHandler) Admit(req *admission.Request) (*admission return f.admitter.Admit(req) } -func (f *fakeMutatingAdmissionHandler) MutatingWebhook(clientConfig v1.WebhookClientConfig) []v1.MutatingWebhook { +func (f *fakeMutatingAdmissionHandler) MutatingWebhook(_ v1.WebhookClientConfig) []v1.MutatingWebhook { return nil } @@ -410,6 +410,6 @@ type fakeAdmitter struct { err error } -func (f *fakeAdmitter) Admit(req *admission.Request) (*admissionv1.AdmissionResponse, error) { +func (f *fakeAdmitter) Admit(_ *admission.Request) (*admissionv1.AdmissionResponse, error) { return &f.response, f.err } diff --git a/pkg/codegen/cleanup/main.go b/pkg/codegen/cleanup/main.go index 68a6b25a..5938a452 100644 --- a/pkg/codegen/cleanup/main.go +++ b/pkg/codegen/cleanup/main.go @@ -10,4 +10,8 @@ func main() { if err := os.RemoveAll("./pkg/generated"); err != nil { logrus.Fatal(err) } + // if we don't have the docs file no need to clean it up + if err := os.Remove("./docs.md"); err != nil && !os.IsNotExist(err) { + logrus.Fatal(err) + } } diff --git a/pkg/codegen/docs.go b/pkg/codegen/docs.go new file mode 100644 index 00000000..d8b9e4ac --- /dev/null +++ b/pkg/codegen/docs.go @@ -0,0 +1,142 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + + "golang.org/x/exp/slices" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +// docFileName defines the name of the files that will be aggregated into overall docs +const docFileExtension = ".md" + +type docFile struct { + content []byte + resource string + group string + version string +} + +func generateDocs(resourcesBaseDir, outputFilePath string) (err error) { + outputFile, err := os.OpenFile(outputFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + defer func() { + closeErr := outputFile.Close() + if closeErr != nil { + if err != nil { + err = fmt.Errorf("%w, error when closing file %s", err, closeErr.Error()) + } else { + err = closeErr + } + } + }() + if err != nil { + return err + } + docFiles, err := getDocFiles(resourcesBaseDir) + if err != nil { + return fmt.Errorf("unable to create documentation: %w", err) + } + currentGroup := "" + for _, docFile := range docFiles { + newGroup := docFile.group + if newGroup != currentGroup { + // our group has changed, output a new group header + groupFormatString := "# %s/%s \n" + if currentGroup != "" { + groupFormatString = "\n" + groupFormatString + } + _, err = fmt.Fprintf(outputFile, groupFormatString, docFile.group, docFile.version) + if err != nil { + return fmt.Errorf("unable to write group header for %s/%s: %w", docFile.group, docFile.version, err) + } + currentGroup = newGroup + } + + _, err = fmt.Fprintf(outputFile, "\n## %s \n\n", docFile.resource) + if err != nil { + return fmt.Errorf("unable to write resource header for %s: %w", docFile.resource, err) + } + scanner := bufio.NewScanner(bytes.NewReader(docFile.content)) + for scanner.Scan() { + line := scanner.Bytes() + // even if the scanned line is empty, still need to output the newline + if len(line) != 0 && line[0] == '#' { + // this line is a markdown header. Since the group header is the top-level indent, indent this down one line + line = append([]byte{'#'}, line...) + } + line = append(line, byte('\n')) + _, err := outputFile.Write(line) + if err != nil { + return fmt.Errorf("unable to write content for %s/%s.%s: %w", docFile.group, docFile.version, docFile.resource, err) + } + + } + + } + return nil +} + +// getDocFiles finds all markdown files recursively in resourcesBaseDir and converts them to docFiles. Returns in a sorted order, +// first by group, then by resourceName +func getDocFiles(baseDir string) ([]docFile, error) { + entries, err := os.ReadDir(baseDir) + if err != nil { + return nil, fmt.Errorf("unable to list entries in directory %s: %w", baseDir, err) + } + var docFiles []docFile + for _, entry := range entries { + entryPath := filepath.Join(baseDir, entry.Name()) + if entry.IsDir() { + subDocFiles, err := getDocFiles(entryPath) + if err != nil { + return nil, err + } + docFiles = append(docFiles, subDocFiles...) + continue + } + if filepath.Ext(entry.Name()) != docFileExtension { + continue + } + content, err := os.ReadFile(filepath.Join(baseDir, entry.Name())) + if err != nil { + return nil, fmt.Errorf("unable to read file content for %s: %w", entryPath, err) + } + // lop off the last trailing new line to keep consistent spacing for later on + if content[len(content)-1] == '\n' { + content = content[:len(content)-1] + } + newDir, _ := filepath.Split(baseDir) + newDir, version := filepath.Split(newDir[:len(newDir)-1]) + newDir, group := filepath.Split(newDir[:len(newDir)-1]) + resource := strings.TrimSuffix(entry.Name(), docFileExtension) + if newDir == "" || resource == "" || version == "" || group == "" { + return nil, fmt.Errorf("unable to extract gvr from %s, got group %s, version %s, resource %s", baseDir, group, version, resource) + } + // group and version need to have a consistent case so that test.cattle.io/v3 and test.cattle.Io/V3 are grouped the same way + caser := cases.Lower(language.English) + docFiles = append(docFiles, docFile{ + content: content, + resource: resource, + group: caser.String(group), + version: caser.String(version), + }) + } + // if the groups differ, sort based on the group. If the groups are the same, sort based on the resource + slices.SortFunc(docFiles, func(a, b docFile) bool { + if a.group == b.group { + if a.resource == b.resource { + return a.version < b.version + } + return a.resource == b.resource + } + return a.group < b.group + }) + + return docFiles, nil +} diff --git a/pkg/codegen/main.go b/pkg/codegen/main.go index a5c93cd9..ae93a53c 100644 --- a/pkg/codegen/main.go +++ b/pkg/codegen/main.go @@ -24,6 +24,10 @@ type typeInfo struct { func main() { os.Unsetenv("GOPATH") + err := generateDocs("pkg/resources", "docs.md") + if err != nil { + panic(err) + } controllergen.Run(args.Options{ OutputPackage: "github.com/rancher/webhook/pkg/generated", Boilerplate: "scripts/boilerplate.go.txt", @@ -36,6 +40,7 @@ func main() { v3.RoleTemplate{}, v3.ClusterRoleTemplateBinding{}, v3.ProjectRoleTemplateBinding{}, + v3.Node{}, }, }, "provisioning.cattle.io": { @@ -59,6 +64,7 @@ func main() { &v3.GlobalRoleBinding{}, &v3.RoleTemplate{}, &v3.ProjectRoleTemplateBinding{}, + &v3.NodeDriver{}, }, }, "provisioning.cattle.io": { diff --git a/pkg/fakes/NodeDriverCache.go b/pkg/fakes/NodeDriverCache.go new file mode 100644 index 00000000..a1e06bd5 --- /dev/null +++ b/pkg/fakes/NodeDriverCache.go @@ -0,0 +1,94 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/rancher/webhook/pkg/generated/controllers/management.cattle.io/v3 (interfaces: NodeCache) + +// Package fakes is a generated GoMock package. +package fakes + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" + v30 "github.com/rancher/webhook/pkg/generated/controllers/management.cattle.io/v3" + labels "k8s.io/apimachinery/pkg/labels" +) + +// MockNodeCache is a mock of NodeCache interface. +type MockNodeCache struct { + ctrl *gomock.Controller + recorder *MockNodeCacheMockRecorder +} + +// MockNodeCacheMockRecorder is the mock recorder for MockNodeCache. +type MockNodeCacheMockRecorder struct { + mock *MockNodeCache +} + +// NewMockNodeCache creates a new mock instance. +func NewMockNodeCache(ctrl *gomock.Controller) *MockNodeCache { + mock := &MockNodeCache{ctrl: ctrl} + mock.recorder = &MockNodeCacheMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockNodeCache) EXPECT() *MockNodeCacheMockRecorder { + return m.recorder +} + +// AddIndexer mocks base method. +func (m *MockNodeCache) AddIndexer(arg0 string, arg1 v30.NodeIndexer) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddIndexer", arg0, arg1) +} + +// AddIndexer indicates an expected call of AddIndexer. +func (mr *MockNodeCacheMockRecorder) AddIndexer(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddIndexer", reflect.TypeOf((*MockNodeCache)(nil).AddIndexer), arg0, arg1) +} + +// Get mocks base method. +func (m *MockNodeCache) Get(arg0, arg1 string) (*v3.Node, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1) + ret0, _ := ret[0].(*v3.Node) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockNodeCacheMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockNodeCache)(nil).Get), arg0, arg1) +} + +// GetByIndex mocks base method. +func (m *MockNodeCache) GetByIndex(arg0, arg1 string) ([]*v3.Node, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByIndex", arg0, arg1) + ret0, _ := ret[0].([]*v3.Node) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByIndex indicates an expected call of GetByIndex. +func (mr *MockNodeCacheMockRecorder) GetByIndex(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByIndex", reflect.TypeOf((*MockNodeCache)(nil).GetByIndex), arg0, arg1) +} + +// List mocks base method. +func (m *MockNodeCache) List(arg0 string, arg1 labels.Selector) ([]*v3.Node, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", arg0, arg1) + ret0, _ := ret[0].([]*v3.Node) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockNodeCacheMockRecorder) List(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockNodeCache)(nil).List), arg0, arg1) +} diff --git a/pkg/fakes/generator.go b/pkg/fakes/generator.go index fc43a2d4..71f2083a 100644 --- a/pkg/fakes/generator.go +++ b/pkg/fakes/generator.go @@ -10,3 +10,4 @@ package fakes //go:generate mockgen --build_flags=--mod=mod -package fakes -destination ./k8Validation.go "k8s.io/kubernetes/pkg/registry/rbac/validation" AuthorizationRuleResolver //go:generate mockgen --build_flags=--mod=mod -package fakes -destination ./RoleCache.go github.com/rancher/wrangler/pkg/generated/controllers/rbac/v1 RoleCache,RoleController //go:generate mockgen --build_flags=--mod=mod -package fakes -destination ./RoleBindingCache.go github.com/rancher/wrangler/pkg/generated/controllers/rbac/v1 RoleBindingCache,RoleBindingController +//go:generate mockgen --build_flags=--mod=mod -package fakes -destination ./NodeDriverCache.go github.com/rancher/webhook/pkg/generated/controllers/management.cattle.io/v3 NodeCache diff --git a/pkg/generated/controllers/management.cattle.io/factory.go b/pkg/generated/controllers/management.cattle.io/factory.go index 7c3285d0..fe6198d6 100644 --- a/pkg/generated/controllers/management.cattle.io/factory.go +++ b/pkg/generated/controllers/management.cattle.io/factory.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by main. DO NOT EDIT. +// Code generated by codegen. DO NOT EDIT. package management diff --git a/pkg/generated/controllers/management.cattle.io/interface.go b/pkg/generated/controllers/management.cattle.io/interface.go index a5ad50d8..1132a589 100644 --- a/pkg/generated/controllers/management.cattle.io/interface.go +++ b/pkg/generated/controllers/management.cattle.io/interface.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by main. DO NOT EDIT. +// Code generated by codegen. DO NOT EDIT. package management diff --git a/pkg/generated/controllers/management.cattle.io/v3/cluster.go b/pkg/generated/controllers/management.cattle.io/v3/cluster.go index a08d9c35..5a0fae11 100644 --- a/pkg/generated/controllers/management.cattle.io/v3/cluster.go +++ b/pkg/generated/controllers/management.cattle.io/v3/cluster.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by main. DO NOT EDIT. +// Code generated by codegen. DO NOT EDIT. package v3 diff --git a/pkg/generated/controllers/management.cattle.io/v3/clusterroletemplatebinding.go b/pkg/generated/controllers/management.cattle.io/v3/clusterroletemplatebinding.go index 8bbdd3c8..8dd06112 100644 --- a/pkg/generated/controllers/management.cattle.io/v3/clusterroletemplatebinding.go +++ b/pkg/generated/controllers/management.cattle.io/v3/clusterroletemplatebinding.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by main. DO NOT EDIT. +// Code generated by codegen. DO NOT EDIT. package v3 diff --git a/pkg/generated/controllers/management.cattle.io/v3/globalrole.go b/pkg/generated/controllers/management.cattle.io/v3/globalrole.go index 47d41e6a..17d923c7 100644 --- a/pkg/generated/controllers/management.cattle.io/v3/globalrole.go +++ b/pkg/generated/controllers/management.cattle.io/v3/globalrole.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by main. DO NOT EDIT. +// Code generated by codegen. DO NOT EDIT. package v3 diff --git a/pkg/generated/controllers/management.cattle.io/v3/interface.go b/pkg/generated/controllers/management.cattle.io/v3/interface.go index b29e7d95..e7f6240d 100644 --- a/pkg/generated/controllers/management.cattle.io/v3/interface.go +++ b/pkg/generated/controllers/management.cattle.io/v3/interface.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by main. DO NOT EDIT. +// Code generated by codegen. DO NOT EDIT. package v3 @@ -33,6 +33,7 @@ type Interface interface { Cluster() ClusterController ClusterRoleTemplateBinding() ClusterRoleTemplateBindingController GlobalRole() GlobalRoleController + Node() NodeController PodSecurityAdmissionConfigurationTemplate() PodSecurityAdmissionConfigurationTemplateController ProjectRoleTemplateBinding() ProjectRoleTemplateBindingController RoleTemplate() RoleTemplateController @@ -57,6 +58,9 @@ func (c *version) ClusterRoleTemplateBinding() ClusterRoleTemplateBindingControl func (c *version) GlobalRole() GlobalRoleController { return NewGlobalRoleController(schema.GroupVersionKind{Group: "management.cattle.io", Version: "v3", Kind: "GlobalRole"}, "globalroles", false, c.controllerFactory) } +func (c *version) Node() NodeController { + return NewNodeController(schema.GroupVersionKind{Group: "management.cattle.io", Version: "v3", Kind: "Node"}, "nodes", true, c.controllerFactory) +} func (c *version) PodSecurityAdmissionConfigurationTemplate() PodSecurityAdmissionConfigurationTemplateController { return NewPodSecurityAdmissionConfigurationTemplateController(schema.GroupVersionKind{Group: "management.cattle.io", Version: "v3", Kind: "PodSecurityAdmissionConfigurationTemplate"}, "podsecurityadmissionconfigurationtemplates", false, c.controllerFactory) } diff --git a/pkg/generated/controllers/management.cattle.io/v3/node.go b/pkg/generated/controllers/management.cattle.io/v3/node.go new file mode 100644 index 00000000..8e88cb5e --- /dev/null +++ b/pkg/generated/controllers/management.cattle.io/v3/node.go @@ -0,0 +1,376 @@ +/* +Copyright 2023 Rancher Labs, Inc. + +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 codegen. DO NOT EDIT. + +package v3 + +import ( + "context" + "time" + + "github.com/rancher/lasso/pkg/client" + "github.com/rancher/lasso/pkg/controller" + v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" + "github.com/rancher/wrangler/pkg/apply" + "github.com/rancher/wrangler/pkg/condition" + "github.com/rancher/wrangler/pkg/generic" + "github.com/rancher/wrangler/pkg/kv" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/tools/cache" +) + +type NodeHandler func(string, *v3.Node) (*v3.Node, error) + +type NodeController interface { + generic.ControllerMeta + NodeClient + + OnChange(ctx context.Context, name string, sync NodeHandler) + OnRemove(ctx context.Context, name string, sync NodeHandler) + Enqueue(namespace, name string) + EnqueueAfter(namespace, name string, duration time.Duration) + + Cache() NodeCache +} + +type NodeClient interface { + Create(*v3.Node) (*v3.Node, error) + Update(*v3.Node) (*v3.Node, error) + UpdateStatus(*v3.Node) (*v3.Node, error) + Delete(namespace, name string, options *metav1.DeleteOptions) error + Get(namespace, name string, options metav1.GetOptions) (*v3.Node, error) + List(namespace string, opts metav1.ListOptions) (*v3.NodeList, error) + Watch(namespace string, opts metav1.ListOptions) (watch.Interface, error) + Patch(namespace, name string, pt types.PatchType, data []byte, subresources ...string) (result *v3.Node, err error) +} + +type NodeCache interface { + Get(namespace, name string) (*v3.Node, error) + List(namespace string, selector labels.Selector) ([]*v3.Node, error) + + AddIndexer(indexName string, indexer NodeIndexer) + GetByIndex(indexName, key string) ([]*v3.Node, error) +} + +type NodeIndexer func(obj *v3.Node) ([]string, error) + +type nodeController struct { + controller controller.SharedController + client *client.Client + gvk schema.GroupVersionKind + groupResource schema.GroupResource +} + +func NewNodeController(gvk schema.GroupVersionKind, resource string, namespaced bool, controller controller.SharedControllerFactory) NodeController { + c := controller.ForResourceKind(gvk.GroupVersion().WithResource(resource), gvk.Kind, namespaced) + return &nodeController{ + controller: c, + client: c.Client(), + gvk: gvk, + groupResource: schema.GroupResource{ + Group: gvk.Group, + Resource: resource, + }, + } +} + +func FromNodeHandlerToHandler(sync NodeHandler) generic.Handler { + return func(key string, obj runtime.Object) (ret runtime.Object, err error) { + var v *v3.Node + if obj == nil { + v, err = sync(key, nil) + } else { + v, err = sync(key, obj.(*v3.Node)) + } + if v == nil { + return nil, err + } + return v, err + } +} + +func (c *nodeController) Updater() generic.Updater { + return func(obj runtime.Object) (runtime.Object, error) { + newObj, err := c.Update(obj.(*v3.Node)) + if newObj == nil { + return nil, err + } + return newObj, err + } +} + +func UpdateNodeDeepCopyOnChange(client NodeClient, obj *v3.Node, handler func(obj *v3.Node) (*v3.Node, error)) (*v3.Node, error) { + if obj == nil { + return obj, nil + } + + copyObj := obj.DeepCopy() + newObj, err := handler(copyObj) + if newObj != nil { + copyObj = newObj + } + if obj.ResourceVersion == copyObj.ResourceVersion && !equality.Semantic.DeepEqual(obj, copyObj) { + return client.Update(copyObj) + } + + return copyObj, err +} + +func (c *nodeController) AddGenericHandler(ctx context.Context, name string, handler generic.Handler) { + c.controller.RegisterHandler(ctx, name, controller.SharedControllerHandlerFunc(handler)) +} + +func (c *nodeController) AddGenericRemoveHandler(ctx context.Context, name string, handler generic.Handler) { + c.AddGenericHandler(ctx, name, generic.NewRemoveHandler(name, c.Updater(), handler)) +} + +func (c *nodeController) OnChange(ctx context.Context, name string, sync NodeHandler) { + c.AddGenericHandler(ctx, name, FromNodeHandlerToHandler(sync)) +} + +func (c *nodeController) OnRemove(ctx context.Context, name string, sync NodeHandler) { + c.AddGenericHandler(ctx, name, generic.NewRemoveHandler(name, c.Updater(), FromNodeHandlerToHandler(sync))) +} + +func (c *nodeController) Enqueue(namespace, name string) { + c.controller.Enqueue(namespace, name) +} + +func (c *nodeController) EnqueueAfter(namespace, name string, duration time.Duration) { + c.controller.EnqueueAfter(namespace, name, duration) +} + +func (c *nodeController) Informer() cache.SharedIndexInformer { + return c.controller.Informer() +} + +func (c *nodeController) GroupVersionKind() schema.GroupVersionKind { + return c.gvk +} + +func (c *nodeController) Cache() NodeCache { + return &nodeCache{ + indexer: c.Informer().GetIndexer(), + resource: c.groupResource, + } +} + +func (c *nodeController) Create(obj *v3.Node) (*v3.Node, error) { + result := &v3.Node{} + return result, c.client.Create(context.TODO(), obj.Namespace, obj, result, metav1.CreateOptions{}) +} + +func (c *nodeController) Update(obj *v3.Node) (*v3.Node, error) { + result := &v3.Node{} + return result, c.client.Update(context.TODO(), obj.Namespace, obj, result, metav1.UpdateOptions{}) +} + +func (c *nodeController) UpdateStatus(obj *v3.Node) (*v3.Node, error) { + result := &v3.Node{} + return result, c.client.UpdateStatus(context.TODO(), obj.Namespace, obj, result, metav1.UpdateOptions{}) +} + +func (c *nodeController) Delete(namespace, name string, options *metav1.DeleteOptions) error { + if options == nil { + options = &metav1.DeleteOptions{} + } + return c.client.Delete(context.TODO(), namespace, name, *options) +} + +func (c *nodeController) Get(namespace, name string, options metav1.GetOptions) (*v3.Node, error) { + result := &v3.Node{} + return result, c.client.Get(context.TODO(), namespace, name, result, options) +} + +func (c *nodeController) List(namespace string, opts metav1.ListOptions) (*v3.NodeList, error) { + result := &v3.NodeList{} + return result, c.client.List(context.TODO(), namespace, result, opts) +} + +func (c *nodeController) Watch(namespace string, opts metav1.ListOptions) (watch.Interface, error) { + return c.client.Watch(context.TODO(), namespace, opts) +} + +func (c *nodeController) Patch(namespace, name string, pt types.PatchType, data []byte, subresources ...string) (*v3.Node, error) { + result := &v3.Node{} + return result, c.client.Patch(context.TODO(), namespace, name, pt, data, result, metav1.PatchOptions{}, subresources...) +} + +type nodeCache struct { + indexer cache.Indexer + resource schema.GroupResource +} + +func (c *nodeCache) Get(namespace, name string) (*v3.Node, error) { + obj, exists, err := c.indexer.GetByKey(namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(c.resource, name) + } + return obj.(*v3.Node), nil +} + +func (c *nodeCache) List(namespace string, selector labels.Selector) (ret []*v3.Node, err error) { + + err = cache.ListAllByNamespace(c.indexer, namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v3.Node)) + }) + + return ret, err +} + +func (c *nodeCache) AddIndexer(indexName string, indexer NodeIndexer) { + utilruntime.Must(c.indexer.AddIndexers(map[string]cache.IndexFunc{ + indexName: func(obj interface{}) (strings []string, e error) { + return indexer(obj.(*v3.Node)) + }, + })) +} + +func (c *nodeCache) GetByIndex(indexName, key string) (result []*v3.Node, err error) { + objs, err := c.indexer.ByIndex(indexName, key) + if err != nil { + return nil, err + } + result = make([]*v3.Node, 0, len(objs)) + for _, obj := range objs { + result = append(result, obj.(*v3.Node)) + } + return result, nil +} + +type NodeStatusHandler func(obj *v3.Node, status v3.NodeStatus) (v3.NodeStatus, error) + +type NodeGeneratingHandler func(obj *v3.Node, status v3.NodeStatus) ([]runtime.Object, v3.NodeStatus, error) + +func RegisterNodeStatusHandler(ctx context.Context, controller NodeController, condition condition.Cond, name string, handler NodeStatusHandler) { + statusHandler := &nodeStatusHandler{ + client: controller, + condition: condition, + handler: handler, + } + controller.AddGenericHandler(ctx, name, FromNodeHandlerToHandler(statusHandler.sync)) +} + +func RegisterNodeGeneratingHandler(ctx context.Context, controller NodeController, apply apply.Apply, + condition condition.Cond, name string, handler NodeGeneratingHandler, opts *generic.GeneratingHandlerOptions) { + statusHandler := &nodeGeneratingHandler{ + NodeGeneratingHandler: handler, + apply: apply, + name: name, + gvk: controller.GroupVersionKind(), + } + if opts != nil { + statusHandler.opts = *opts + } + controller.OnChange(ctx, name, statusHandler.Remove) + RegisterNodeStatusHandler(ctx, controller, condition, name, statusHandler.Handle) +} + +type nodeStatusHandler struct { + client NodeClient + condition condition.Cond + handler NodeStatusHandler +} + +func (a *nodeStatusHandler) sync(key string, obj *v3.Node) (*v3.Node, error) { + if obj == nil { + return obj, nil + } + + origStatus := obj.Status.DeepCopy() + obj = obj.DeepCopy() + newStatus, err := a.handler(obj, obj.Status) + if err != nil { + // Revert to old status on error + newStatus = *origStatus.DeepCopy() + } + + if a.condition != "" { + if errors.IsConflict(err) { + a.condition.SetError(&newStatus, "", nil) + } else { + a.condition.SetError(&newStatus, "", err) + } + } + if !equality.Semantic.DeepEqual(origStatus, &newStatus) { + if a.condition != "" { + // Since status has changed, update the lastUpdatedTime + a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) + } + + var newErr error + obj.Status = newStatus + newObj, newErr := a.client.UpdateStatus(obj) + if err == nil { + err = newErr + } + if newErr == nil { + obj = newObj + } + } + return obj, err +} + +type nodeGeneratingHandler struct { + NodeGeneratingHandler + apply apply.Apply + opts generic.GeneratingHandlerOptions + gvk schema.GroupVersionKind + name string +} + +func (a *nodeGeneratingHandler) Remove(key string, obj *v3.Node) (*v3.Node, error) { + if obj != nil { + return obj, nil + } + + obj = &v3.Node{} + obj.Namespace, obj.Name = kv.RSplit(key, "/") + obj.SetGroupVersionKind(a.gvk) + + return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). + WithOwner(obj). + WithSetID(a.name). + ApplyObjects() +} + +func (a *nodeGeneratingHandler) Handle(obj *v3.Node, status v3.NodeStatus) (v3.NodeStatus, error) { + if !obj.DeletionTimestamp.IsZero() { + return status, nil + } + + objs, newStatus, err := a.NodeGeneratingHandler(obj, status) + if err != nil { + return newStatus, err + } + + return newStatus, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). + WithOwner(obj). + WithSetID(a.name). + ApplyObjects(objs...) +} diff --git a/pkg/generated/controllers/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate.go b/pkg/generated/controllers/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate.go index 37d472af..01855e25 100644 --- a/pkg/generated/controllers/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate.go +++ b/pkg/generated/controllers/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by main. DO NOT EDIT. +// Code generated by codegen. DO NOT EDIT. package v3 diff --git a/pkg/generated/controllers/management.cattle.io/v3/projectroletemplatebinding.go b/pkg/generated/controllers/management.cattle.io/v3/projectroletemplatebinding.go index 20796765..cfe82286 100644 --- a/pkg/generated/controllers/management.cattle.io/v3/projectroletemplatebinding.go +++ b/pkg/generated/controllers/management.cattle.io/v3/projectroletemplatebinding.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by main. DO NOT EDIT. +// Code generated by codegen. DO NOT EDIT. package v3 diff --git a/pkg/generated/controllers/management.cattle.io/v3/roletemplate.go b/pkg/generated/controllers/management.cattle.io/v3/roletemplate.go index 32392aca..eb1640bd 100644 --- a/pkg/generated/controllers/management.cattle.io/v3/roletemplate.go +++ b/pkg/generated/controllers/management.cattle.io/v3/roletemplate.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by main. DO NOT EDIT. +// Code generated by codegen. DO NOT EDIT. package v3 diff --git a/pkg/generated/controllers/provisioning.cattle.io/factory.go b/pkg/generated/controllers/provisioning.cattle.io/factory.go index b94972d2..cb3cf9f5 100644 --- a/pkg/generated/controllers/provisioning.cattle.io/factory.go +++ b/pkg/generated/controllers/provisioning.cattle.io/factory.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by main. DO NOT EDIT. +// Code generated by codegen. DO NOT EDIT. package provisioning diff --git a/pkg/generated/controllers/provisioning.cattle.io/interface.go b/pkg/generated/controllers/provisioning.cattle.io/interface.go index 1b7b6f10..f63f0357 100644 --- a/pkg/generated/controllers/provisioning.cattle.io/interface.go +++ b/pkg/generated/controllers/provisioning.cattle.io/interface.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by main. DO NOT EDIT. +// Code generated by codegen. DO NOT EDIT. package provisioning diff --git a/pkg/generated/controllers/provisioning.cattle.io/v1/cluster.go b/pkg/generated/controllers/provisioning.cattle.io/v1/cluster.go index 0c30af60..1f762e5b 100644 --- a/pkg/generated/controllers/provisioning.cattle.io/v1/cluster.go +++ b/pkg/generated/controllers/provisioning.cattle.io/v1/cluster.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by main. DO NOT EDIT. +// Code generated by codegen. DO NOT EDIT. package v1 diff --git a/pkg/generated/controllers/provisioning.cattle.io/v1/interface.go b/pkg/generated/controllers/provisioning.cattle.io/v1/interface.go index f2f2e1be..19bd91ed 100644 --- a/pkg/generated/controllers/provisioning.cattle.io/v1/interface.go +++ b/pkg/generated/controllers/provisioning.cattle.io/v1/interface.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by main. DO NOT EDIT. +// Code generated by codegen. DO NOT EDIT. package v1 diff --git a/pkg/generated/objects/management.cattle.io/v3/objects.go b/pkg/generated/objects/management.cattle.io/v3/objects.go index 81b37b10..30b21a2e 100644 --- a/pkg/generated/objects/management.cattle.io/v3/objects.go +++ b/pkg/generated/objects/management.cattle.io/v3/objects.go @@ -484,3 +484,56 @@ func ProjectRoleTemplateBindingFromRequest(request *admissionv1.AdmissionRequest return object, nil } + +// NodeDriverOldAndNewFromRequest gets the old and new NodeDriver objects, respectively, from the webhook request. +// If the request is a Delete operation, then the new object is the zero value for NodeDriver. +// Similarly, if the request is a Create operation, then the old object is the zero value for NodeDriver. +func NodeDriverOldAndNewFromRequest(request *admissionv1.AdmissionRequest) (*v3.NodeDriver, *v3.NodeDriver, error) { + if request == nil { + return nil, nil, fmt.Errorf("nil request") + } + + object := &v3.NodeDriver{} + oldObject := &v3.NodeDriver{} + + if request.Operation != admissionv1.Delete { + err := json.Unmarshal(request.Object.Raw, object) + if err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal request object: %w", err) + } + } + + if request.Operation == admissionv1.Create { + return oldObject, object, nil + } + + err := json.Unmarshal(request.OldObject.Raw, oldObject) + if err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal request oldObject: %w", err) + } + + return oldObject, object, nil +} + +// NodeDriverFromRequest returns a NodeDriver object from the webhook request. +// If the operation is a Delete operation, then the old object is returned. +// Otherwise, the new object is returned. +func NodeDriverFromRequest(request *admissionv1.AdmissionRequest) (*v3.NodeDriver, error) { + if request == nil { + return nil, fmt.Errorf("nil request") + } + + object := &v3.NodeDriver{} + raw := request.Object.Raw + + if request.Operation == admissionv1.Delete { + raw = request.OldObject.Raw + } + + err := json.Unmarshal(raw, object) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal request object: %w", err) + } + + return object, nil +} diff --git a/pkg/mocks/RoleTemplateCache.go b/pkg/mocks/RoleTemplateCache.go index 8339396c..9c9784d2 100644 --- a/pkg/mocks/RoleTemplateCache.go +++ b/pkg/mocks/RoleTemplateCache.go @@ -13,26 +13,15 @@ import ( type MockRoleTemplateCache struct { // state is the internal state used to store our role templates for test purposes state []*v3.RoleTemplate - // errAfterNext determines after how many calls to the cache an error will be thrown. If -1 don't ever throw error - errAfterNext int - // nextError is the next error that will be returned after errAfterNext calls - nextError error } func NewMockRoleTemplateCache() *MockRoleTemplateCache { return &MockRoleTemplateCache{ - state: []*v3.RoleTemplate{}, - errAfterNext: -1, - nextError: nil, + state: []*v3.RoleTemplate{}, } } func (mc *MockRoleTemplateCache) Get(name string) (*v3.RoleTemplate, error) { - if mc.shouldReturnErr() { - mc.decrementErrorCounter() - return nil, mc.nextError - } - mc.decrementErrorCounter() for _, template := range mc.state { if template.Name == name { return template, nil @@ -41,37 +30,17 @@ func (mc *MockRoleTemplateCache) Get(name string) (*v3.RoleTemplate, error) { return nil, apierrors.NewNotFound(schema.GroupResource{Group: "management.cattle.io", Resource: "roletemplate"}, name) } -func (mc *MockRoleTemplateCache) List(selector labels.Selector) ([]*v3.RoleTemplate, error) { - if mc.shouldReturnErr() { - mc.decrementErrorCounter() - return nil, mc.nextError - } - mc.decrementErrorCounter() +func (mc *MockRoleTemplateCache) List(_ labels.Selector) ([]*v3.RoleTemplate, error) { return mc.state, nil } -func (mc *MockRoleTemplateCache) AddIndexer(indexName string, indexer controllerv3.RoleTemplateIndexer) { - //TODO: Add indexer method +func (mc *MockRoleTemplateCache) AddIndexer(string, controllerv3.RoleTemplateIndexer) { } -func (mc *MockRoleTemplateCache) GetByIndex(indexName, key string) ([]*v3.RoleTemplate, error) { - //TODO: Add GetByIndexer method +func (mc *MockRoleTemplateCache) GetByIndex(string, string) ([]*v3.RoleTemplate, error) { return nil, fmt.Errorf("not implemented") } func (mc *MockRoleTemplateCache) Add(template *v3.RoleTemplate) { mc.state = append(mc.state, template) } - -func (mc *MockRoleTemplateCache) AddErr(calls int, err error) { - mc.errAfterNext = calls - mc.nextError = err -} - -func (mc *MockRoleTemplateCache) shouldReturnErr() bool { - return false -} - -func (mc *MockRoleTemplateCache) decrementErrorCounter() { - -} diff --git a/pkg/resolvers/crtbResolver.go b/pkg/resolvers/crtbResolver.go index 1bd94198..ff91ffca 100644 --- a/pkg/resolvers/crtbResolver.go +++ b/pkg/resolvers/crtbResolver.go @@ -33,7 +33,7 @@ func NewCRTBRuleResolver(crtbCache v3.ClusterRoleTemplateBindingCache, roleTempl // GetRoleReferenceRules is used to find which roles are granted by a rolebinding/clusterrolebinding. Since we don't // use these primitives to refer to role templates return empty list. -func (c *CRTBRuleResolver) GetRoleReferenceRules(roleRef rbacv1.RoleRef, namespace string) ([]rbacv1.PolicyRule, error) { +func (c *CRTBRuleResolver) GetRoleReferenceRules(rbacv1.RoleRef, string) ([]rbacv1.PolicyRule, error) { return []rbacv1.PolicyRule{}, nil } diff --git a/pkg/resolvers/prtbResolver.go b/pkg/resolvers/prtbResolver.go index 23d2e78c..4d8d4a51 100644 --- a/pkg/resolvers/prtbResolver.go +++ b/pkg/resolvers/prtbResolver.go @@ -34,7 +34,7 @@ func NewPRTBRuleResolver(prtbCache v3.ProjectRoleTemplateBindingCache, roleTempl // GetRoleReferenceRules is used to find which roles are granted by a rolebinding/clusterrolebinding. Since we don't // use these primitives to refer to role templates return empty list. -func (p *PRTBRuleResolver) GetRoleReferenceRules(roleRef rbacv1.RoleRef, namespace string) ([]rbacv1.PolicyRule, error) { +func (p *PRTBRuleResolver) GetRoleReferenceRules(rbacv1.RoleRef, string) ([]rbacv1.PolicyRule, error) { return []rbacv1.PolicyRule{}, nil } diff --git a/pkg/resolvers/resolvers.go b/pkg/resolvers/resolvers.go index 51b45927..80145eab 100644 --- a/pkg/resolvers/resolvers.go +++ b/pkg/resolvers/resolvers.go @@ -14,7 +14,7 @@ type ruleAccumulator struct { errors []error } -func (r *ruleAccumulator) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool { +func (r *ruleAccumulator) visit(_ fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool { if rule != nil { r.rules = append(r.rules, *rule) } diff --git a/pkg/resources/core/v1/namespace/Namespace.md b/pkg/resources/core/v1/namespace/Namespace.md new file mode 100644 index 00000000..28c45653 --- /dev/null +++ b/pkg/resources/core/v1/namespace/Namespace.md @@ -0,0 +1,10 @@ +## Validation Checks + +Note: The kube-system namespace, unlike other namespaces, has a "failPolicy" of "ignore" on update calls. + +### PSA Label Validation + +Validates that users who create or edit a PSA enforcement label on a namespace have the `updatepsa` verb on `projects` in `management.cattle.io/v3`. See the [upstream docs](https://kubernetes.io/docs/concepts/security/pod-security-admission/) for more information on the effect of these labels. + +The following labels are considered relevant labels for PSA enforcement: `"pod-security.kubernetes.io/enforce", "pod-security.kubernetes.io/enforce-version", "pod-security.kubernetes.io/audit", "pod-security.kubernetes.io/audit-version", "pod-security.kubernetes.io/warn", "pod-security.kubernetes.io/warn-version"`. + diff --git a/pkg/resources/core/v1/secret/mutator.go b/pkg/resources/core/v1/secret/mutator.go index 74866d76..b2d73a9f 100644 --- a/pkg/resources/core/v1/secret/mutator.go +++ b/pkg/resources/core/v1/secret/mutator.go @@ -94,7 +94,7 @@ func (m *Mutator) Admit(request *admission.Request) (*admissionv1.AdmissionRespo case admissionv1.Create: return m.admitCreate(secret, request) case admissionv1.Delete: - return m.admitDelete(secret, request) + return m.admitDelete(secret) default: return nil, fmt.Errorf("operation type %q not handled", request.Operation) } @@ -126,7 +126,7 @@ func (m *Mutator) admitCreate(secret *corev1.Secret, request *admission.Request) // admitDelete checks to see if there are any roleBindings owned by this secret which provide access to a role granting access to this secret // if so, it redacts the role so that it only grants delete access. This handles cases where users were given owner access to an individual secret // through a controller (like cloud-credentials), and delete the secret but keep the rbac -func (m *Mutator) admitDelete(secret *corev1.Secret, request *admission.Request) (*admissionv1.AdmissionResponse, error) { +func (m *Mutator) admitDelete(secret *corev1.Secret) (*admissionv1.AdmissionResponse, error) { roleBindings, err := m.roleBindingController.Cache().GetByIndex(mutatorRoleBindingOwnerIndex, fmt.Sprintf(ownerFormat, secret.Namespace, secret.Name)) if err != nil { return nil, fmt.Errorf("unable to determine if secret %s/%s has rbac references: %w", secret.Namespace, secret.Name, err) diff --git a/pkg/resources/management.cattle.io/v3/clusterroletemplatebinding/ClusterRoleTemplateBinding.md b/pkg/resources/management.cattle.io/v3/clusterroletemplatebinding/ClusterRoleTemplateBinding.md new file mode 100644 index 00000000..ab102270 --- /dev/null +++ b/pkg/resources/management.cattle.io/v3/clusterroletemplatebinding/ClusterRoleTemplateBinding.md @@ -0,0 +1,28 @@ +## Validation Checks + +### Escalation Prevention + +Users can only create/update ClusterRoleTemplateBindings which grant permissions to RoleTemplates with rights less than or equal to those they currently possess. This is to prevent privilege escalation. + +### Invalid Fields - Create + +Users cannot create ClusterRoleTemplateBindings which violate the following constraints: +- Either a user subject (through "UserName" or "UserPrincipalName") or a group subject (through "GroupName" or "GroupPrincipalName") must be specified; both a user subject and group subject cannot be specified +- A "ClusterName" must be specified +- The roleTemplate indicated in "RoleTemplateName" must be: + - Valid (i.e. is an existing `roleTemplate` object in the `management.cattle.io/v3` apiGroup) + - Not locked (i.e. `roleTemplate.Locked` must be `false`) + +### Invalid Fields - Update + +Users cannot update the following fields after creation: +- RoleTemplateName +- ClusterName + +Users can update the following fields if they have not been set, but after they have been set they cannot be changed: +- UserName +- UserPrincipalName +- GroupName +- GroupPrincipalName + +In addition, as in the create validation, both a user subject and a group subject cannot be specified. diff --git a/pkg/resources/management.cattle.io/v3/globalrole/GlobalRole.md b/pkg/resources/management.cattle.io/v3/globalrole/GlobalRole.md new file mode 100644 index 00000000..46fc5ab4 --- /dev/null +++ b/pkg/resources/management.cattle.io/v3/globalrole/GlobalRole.md @@ -0,0 +1,8 @@ +## Validation Checks + +Note: all checks are bypassed if the GlobalRole is being deleted + +### Escalation Prevention + +Users can only change GlobalRoles with rights less than or equal to those they currently possess. This is to prevent privilege escalation. + diff --git a/pkg/resources/management.cattle.io/v3/globalrolebinding/GlobalRoleBinding.md b/pkg/resources/management.cattle.io/v3/globalrolebinding/GlobalRoleBinding.md new file mode 100644 index 00000000..a603b03b --- /dev/null +++ b/pkg/resources/management.cattle.io/v3/globalrolebinding/GlobalRoleBinding.md @@ -0,0 +1,11 @@ +## Validation Checks + +Note: all checks are bypassed if the GlobalRoleBinding is being deleted + +### Escalation Prevention + +Users can only create/update GlobalRoleBindings with rights less than or equal to those they currently possess. This is to prevent privilege escalation. + +### Valid Global Role Reference + +GlobalRoleBindings must refer to a valid global role (i.e. an existing `GlobalRole` object in the `management.cattle.io/v3` apiGroup). diff --git a/pkg/resources/management.cattle.io/v3/nodedriver/NodeDriver.md b/pkg/resources/management.cattle.io/v3/nodedriver/NodeDriver.md new file mode 100644 index 00000000..4dc5e97f --- /dev/null +++ b/pkg/resources/management.cattle.io/v3/nodedriver/NodeDriver.md @@ -0,0 +1,7 @@ +## Validation Checks + +Note: checks only run if a node driver is being disabled or deleted + +### Machine Deletion Prevention + +This admission webhook prevents the disabling or deletion of a NodeDriver if there are any Nodes that are under management by said driver. If there are _any_ nodes that use the driver the request will be denied. diff --git a/pkg/resources/management.cattle.io/v3/nodedriver/validator.go b/pkg/resources/management.cattle.io/v3/nodedriver/validator.go new file mode 100644 index 00000000..dde6b1d6 --- /dev/null +++ b/pkg/resources/management.cattle.io/v3/nodedriver/validator.go @@ -0,0 +1,153 @@ +// Package nodedriver handles validation and creation for rke1 and rke2 nodedrivers. +package nodedriver + +import ( + "fmt" + + "github.com/rancher/lasso/pkg/dynamic" + v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" + "github.com/rancher/webhook/pkg/admission" + controllersv3 "github.com/rancher/webhook/pkg/generated/controllers/management.cattle.io/v3" + objectsv3 "github.com/rancher/webhook/pkg/generated/objects/management.cattle.io/v3" + admissionv1 "k8s.io/api/admission/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + gvr = schema.GroupVersionResource{ + Group: "management.cattle.io", + Version: "v3", + Resource: "nodedrivers", + } + + driverInUse = &admissionv1.AdmissionResponse{ + Result: &metav1.Status{ + Status: metav1.StatusFailure, + Message: "This driver is in use by existing nodes and cannot be disabled", + }, + Allowed: false, + } +) + +// Validator ValidatingWebhook for NodeDrivers +type Validator struct { + admitter admitter +} + +type admitter struct { + nodeCache controllersv3.NodeCache + dynamic dynamicLister +} + +// dynamicLister is an interface to abstract away how we list dynamic objects from k8s +type dynamicLister interface { + List(gvk schema.GroupVersionKind, namespace string, selector labels.Selector) ([]runtime.Object, error) +} + +// NewValidator returns a new Validator for NodeDriver resources +func NewValidator(nodeCache controllersv3.NodeCache, dynamic *dynamic.Controller) admission.ValidatingAdmissionHandler { + return &Validator{admitter: admitter{ + nodeCache: nodeCache, + dynamic: dynamic, + }} +} + +// GVR returns the GroupVersionKind for this CRD. +func (v *Validator) GVR() schema.GroupVersionResource { + return gvr +} + +// Operations returns list of operations handled by this validator. +func (v *Validator) Operations() []admissionregistrationv1.OperationType { + return []admissionregistrationv1.OperationType{admissionregistrationv1.Update, admissionregistrationv1.Delete} +} + +// ValidatingWebhook returns the ValidatingWebhook used for this CRD. +func (v *Validator) ValidatingWebhook(clientConfig admissionregistrationv1.WebhookClientConfig) []admissionregistrationv1.ValidatingWebhook { + return []admissionregistrationv1.ValidatingWebhook{*admission.NewDefaultValidatingWebhook(v, clientConfig, admissionregistrationv1.ClusterScope, v.Operations())} +} + +// Admitters returns the admitter objects used to validate machineconfigs. +func (v *Validator) Admitters() []admission.Admitter { + return []admission.Admitter{&v.admitter} +} + +// Admit is the entrypoint for the validator. Admit will return an error if it unable to process the request. +// If this function is called without NewValidator(..) calls will panic. +func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) { + oldObject, newObject, err := objectsv3.NodeDriverOldAndNewFromRequest(&request.AdmissionRequest) + if err != nil { + return nil, fmt.Errorf("failed to decode object from request: %w", err) + } + + // the check to see if the driver is being disabled is either when we're + // running a delete operation OR an update operation where the active flag + // toggles from true -> false + if !(request.Operation == admissionv1.Delete && oldObject.Spec.Active) && + !(request.Operation == admissionv1.Update && !newObject.Spec.Active && oldObject.Spec.Active) { + return admission.ResponseAllowed(), nil + } + + // check if all node resources have been deleted for both cluster types + rke1Deleted, err := a.rke1ResourcesDeleted(oldObject) + if err != nil { + return nil, err + } + rke2Deleted, err := a.rke2ResourcesDeleted(oldObject) + if err != nil { + return nil, err + } + + if !(rke1Deleted && rke2Deleted) { + return driverInUse, nil + } + + return admission.ResponseAllowed(), nil +} + +// // RKE1 +// this one is a bit more clean since we're just looking at nodes with +// the provider +func (a *admitter) rke1ResourcesDeleted(driver *v3.NodeDriver) (bool, error) { + nodes, err := a.nodeCache.List("", labels.Everything()) + if err != nil { + return false, fmt.Errorf("error listing nodes from cache: %w", err) + } + + for _, node := range nodes { + if node.Status.NodeTemplateSpec == nil { + continue + } + + if node.Status.NodeTemplateSpec.Driver == driver.Spec.DisplayName { + return false, nil + } + } + + return true, nil +} + +// // RKE2 +// this one is pretty weird since we have to get the name of the CR we're +// looking from the displayName of the driver. +func (a *admitter) rke2ResourcesDeleted(driver *v3.NodeDriver) (bool, error) { + gvk := schema.GroupVersionKind{ + Group: "rke-machine.cattle.io", + Version: "v1", + Kind: driver.Spec.DisplayName + "machine", + } + machines, err := a.dynamic.List(gvk, "", labels.Everything()) + if err != nil { + return false, fmt.Errorf("error listing %smachines: %w", driver.Spec.DisplayName, err) + } + + if len(machines) != 0 { + return false, nil + } + + return true, nil +} diff --git a/pkg/resources/management.cattle.io/v3/nodedriver/validator_test.go b/pkg/resources/management.cattle.io/v3/nodedriver/validator_test.go new file mode 100644 index 00000000..2f279cd4 --- /dev/null +++ b/pkg/resources/management.cattle.io/v3/nodedriver/validator_test.go @@ -0,0 +1,205 @@ +package nodedriver + +import ( + "context" + "encoding/json" + "testing" + + "github.com/golang/mock/gomock" + v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" + "github.com/rancher/webhook/pkg/admission" + "github.com/rancher/webhook/pkg/fakes" + "github.com/stretchr/testify/suite" + admissionv1 "k8s.io/api/admission/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type NodeDriverValidationSuite struct { + suite.Suite +} + +func TestNodeDriverValidation(t *testing.T) { + t.Parallel() + suite.Run(t, new(NodeDriverValidationSuite)) +} + +type mockLister struct { + toReturn []runtime.Object +} + +func (m *mockLister) List(_ schema.GroupVersionKind, _ string, _ labels.Selector) ([]runtime.Object, error) { + return m.toReturn, nil +} + +func (suite *NodeDriverValidationSuite) TestHappyPath() { + ctrl := gomock.NewController(suite.T()) + mockCache := fakes.NewMockNodeCache(ctrl) + mockCache.EXPECT().List("", labels.Everything()).Return([]*v3.Node{}, nil) + + a := admitter{ + nodeCache: mockCache, + dynamic: &mockLister{}, + } + + resp, err := a.Admit(&admission.Request{ + Context: context.Background(), + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Update, + OldObject: runtime.RawExtension{Raw: newNodeDriver(true, nil)}, + Object: runtime.RawExtension{Raw: newNodeDriver(false, nil)}, + }}) + + suite.Nil(err) + suite.True(resp.Allowed, "admission request was denied") +} + +func (suite *NodeDriverValidationSuite) TestRKE1NotDeleted() { + ctrl := gomock.NewController(suite.T()) + mockCache := fakes.NewMockNodeCache(ctrl) + mockCache.EXPECT().List("", labels.Everything()).Return([]*v3.Node{ + {Status: v3.NodeStatus{NodeTemplateSpec: &v3.NodeTemplateSpec{ + Driver: "testing", + }}}, + }, nil) + + a := admitter{ + nodeCache: mockCache, + dynamic: &mockLister{}, + } + + resp, err := a.Admit(&admission.Request{ + Context: context.Background(), + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Update, + OldObject: runtime.RawExtension{Raw: newNodeDriver(true, nil)}, + Object: runtime.RawExtension{Raw: newNodeDriver(false, nil)}, + }}) + + suite.Nil(err) + suite.False(resp.Allowed, "admission request was allowed through") +} + +func (suite *NodeDriverValidationSuite) TestRKE2NotDeleted() { + ctrl := gomock.NewController(suite.T()) + mockCache := fakes.NewMockNodeCache(ctrl) + mockCache.EXPECT().List("", labels.Everything()).Return([]*v3.Node{}, nil) + + a := admitter{ + nodeCache: mockCache, + dynamic: &mockLister{toReturn: []runtime.Object{&runtime.Unknown{}}}, + } + + resp, err := a.Admit(&admission.Request{ + Context: context.Background(), + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Update, + OldObject: runtime.RawExtension{Raw: newNodeDriver(true, nil)}, + Object: runtime.RawExtension{Raw: newNodeDriver(false, nil)}, + }}) + + suite.Nil(err) + suite.False(resp.Allowed, "admission request was allowed through") +} + +func (suite *NodeDriverValidationSuite) TestNotDisablingDriver() { + a := admitter{} + resp, err := a.Admit(&admission.Request{ + Context: context.Background(), + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Update, + OldObject: runtime.RawExtension{Raw: newNodeDriver(true, nil)}, + Object: runtime.RawExtension{Raw: newNodeDriver(true, nil)}, + }}) + + suite.Nil(err) + suite.True(resp.Allowed, "admission request was denied") +} + +func (suite *NodeDriverValidationSuite) TestDeleteGood() { + ctrl := gomock.NewController(suite.T()) + mockCache := fakes.NewMockNodeCache(ctrl) + mockCache.EXPECT().List("", labels.Everything()).Return([]*v3.Node{}, nil) + + a := admitter{ + nodeCache: mockCache, + dynamic: &mockLister{}, + } + + resp, err := a.Admit(&admission.Request{ + Context: context.Background(), + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Delete, + OldObject: runtime.RawExtension{Raw: newNodeDriver(true, nil)}, + }}) + + suite.Nil(err) + suite.True(resp.Allowed, "admission request was denied") +} + +func (suite *NodeDriverValidationSuite) TestDeleteRKE1Bad() { + ctrl := gomock.NewController(suite.T()) + mockCache := fakes.NewMockNodeCache(ctrl) + mockCache.EXPECT().List("", labels.Everything()).Return([]*v3.Node{ + {Status: v3.NodeStatus{NodeTemplateSpec: &v3.NodeTemplateSpec{ + Driver: "testing", + }}}, + }, nil) + + a := admitter{ + nodeCache: mockCache, + dynamic: &mockLister{}, + } + + resp, err := a.Admit(&admission.Request{ + Context: context.Background(), + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Delete, + OldObject: runtime.RawExtension{Raw: newNodeDriver(true, nil)}, + }}) + + suite.Nil(err) + suite.False(resp.Allowed, "admission request was allowed") +} + +func (suite *NodeDriverValidationSuite) TestDeleteRKE2Bad() { + ctrl := gomock.NewController(suite.T()) + mockCache := fakes.NewMockNodeCache(ctrl) + mockCache.EXPECT().List("", labels.Everything()).Return([]*v3.Node{}, nil) + + a := admitter{ + nodeCache: mockCache, + dynamic: &mockLister{toReturn: []runtime.Object{&runtime.Unknown{}}}, + } + + resp, err := a.Admit(&admission.Request{ + Context: context.Background(), + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Delete, + OldObject: runtime.RawExtension{Raw: newNodeDriver(true, nil)}, + }}) + + suite.Nil(err) + suite.False(resp.Allowed, "admission request was allowed") +} + +func newNodeDriver(active bool, annotations map[string]string) []byte { + if annotations == nil { + annotations = map[string]string{} + } + + driver := v3.NodeDriver{ + ObjectMeta: v1.ObjectMeta{ + Annotations: annotations, + }, + Spec: v3.NodeDriverSpec{ + DisplayName: "testing", + Active: active, + }, + } + + b, _ := json.Marshal(&driver) + return b +} diff --git a/pkg/resources/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate/validator.go b/pkg/resources/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate/validator.go index aae37216..774adcb7 100644 --- a/pkg/resources/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate/validator.go +++ b/pkg/resources/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate/validator.go @@ -226,11 +226,7 @@ func (a *admitter) validateConfiguration(configurationTemplate *mgmtv3.PodSecuri return err } - if err := validateNamespaces(configurationTemplate).ToAggregate(); err != nil { - return err - } - - return nil + return validateNamespaces(configurationTemplate).ToAggregate() } func validateLevel(p *field.Path, value string) field.ErrorList { diff --git a/pkg/resources/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate/validator_test.go b/pkg/resources/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate/validator_test.go index 5bf1a93f..0f751346 100644 --- a/pkg/resources/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate/validator_test.go +++ b/pkg/resources/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate/validator_test.go @@ -603,22 +603,22 @@ func createRequest(obj *v3.PodSecurityAdmissionConfigurationTemplate, operation type mockMgmtCache struct{} -func (m mockMgmtCache) Get(name string) (*v3.Cluster, error) { +func (m mockMgmtCache) Get(string) (*v3.Cluster, error) { // intentionally unimplemented panic("implement me") } -func (m mockMgmtCache) List(selector labels.Selector) ([]*v3.Cluster, error) { +func (m mockMgmtCache) List(labels.Selector) ([]*v3.Cluster, error) { // intentionally unimplemented panic("implement me") } -func (m mockMgmtCache) AddIndexer(indexName string, indexer controllerv3.ClusterIndexer) { +func (m mockMgmtCache) AddIndexer(string, controllerv3.ClusterIndexer) { // intentionally unimplemented panic("implement me") } -func (m mockMgmtCache) GetByIndex(indexName, key string) ([]*v3.Cluster, error) { +func (m mockMgmtCache) GetByIndex(_, key string) ([]*v3.Cluster, error) { x := []*v3.Cluster{ { Spec: v3.ClusterSpec{ @@ -637,22 +637,22 @@ func (m mockMgmtCache) GetByIndex(indexName, key string) ([]*v3.Cluster, error) type mockProvisioningCache struct{} -func (m mockProvisioningCache) Get(namespace, name string) (*provv1.Cluster, error) { +func (m mockProvisioningCache) Get(string, string) (*provv1.Cluster, error) { // intentionally unimplemented panic("implement me") } -func (m mockProvisioningCache) List(namespace string, selector labels.Selector) ([]*provv1.Cluster, error) { +func (m mockProvisioningCache) List(string, labels.Selector) ([]*provv1.Cluster, error) { // intentionally unimplemented panic("implement me") } -func (m mockProvisioningCache) AddIndexer(indexName string, indexer v1.ClusterIndexer) { +func (m mockProvisioningCache) AddIndexer(string, v1.ClusterIndexer) { // intentionally unimplemented panic("implement me") } -func (m mockProvisioningCache) GetByIndex(indexName, key string) ([]*provv1.Cluster, error) { +func (m mockProvisioningCache) GetByIndex(_, key string) ([]*provv1.Cluster, error) { x := []*provv1.Cluster{ { Spec: provv1.ClusterSpec{ diff --git a/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/ProjectRoleTemplateBinding.md b/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/ProjectRoleTemplateBinding.md new file mode 100644 index 00000000..ce2dc5d7 --- /dev/null +++ b/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/ProjectRoleTemplateBinding.md @@ -0,0 +1,28 @@ +## Validation Checks + +### Escalation Prevention + +Users can only create/update ProjectRoleTemplateBindings with rights less than or equal to those they currently possess. This is to prevent privilege escalation. + +### Invalid Fields - Create + +Users cannot create ProjectRoleTemplateBindings which violate the following constraints: +- Either a user subject (through "UserName" or "UserPrincipalName") or a group subject (through "GroupName" or "GroupPrincipalName") must be specified; both a user subject and group subject cannot be specified +- A "ProjectName" must be specified +- The roleTemplate indicated in "RoleTemplateName" must be: + - Valid (i.e. is an existing `roleTemplate` object in the `management.cattle.io/v3` apiGroup) + - Not locked (i.e. `roleTemplate.Locked` must be `false`) + +### Invalid Fields - Update + +Users cannot update the following fields after creation: +- RoleTemplateName +- ProjectName + +Users can update the following fields if they have not been set, but after they have been set they cannot be changed: +- UserName +- UserPrincipalName +- GroupName +- GroupPrincipalName + +In addition, as in the create validation, both a user subject and a group subject cannot be specified. diff --git a/pkg/resources/management.cattle.io/v3/roletemplate/RoleTemplate.md b/pkg/resources/management.cattle.io/v3/roletemplate/RoleTemplate.md new file mode 100644 index 00000000..c820fe8c --- /dev/null +++ b/pkg/resources/management.cattle.io/v3/roletemplate/RoleTemplate.md @@ -0,0 +1,16 @@ +## Validation Checks + +Note: all checks are bypassed if the RoleTemplate is being deleted + +### Circular Reference + +Circular references to webhooks (a inherits b, b inherits a) are not allowed. More specifically, if "roleTemplate1" is included in the `roleTemplateNames` of "roleTemplate2", then "roleTemplate2" must not be included in the `roleTemplateNames` of "roleTemplate1". This checks prevents the creation of roles whose end-state cannot be resolved. + +### Rules Without Verbs + +Rules without verbs are not permitted. The `rules` included in a roleTemplate are of the same type as the rules used by standard kubernetes RBAC types (such as `Roles` from `rbac.authorization.k8s.io/v1`). Because of this, they inherit the same restrictions as these types, including this one. + +### Escalation Prevention + +Users can only change RoleTemplates with rights less than or equal to those they currently possess. This prevents privilege escalation. + diff --git a/pkg/server/handlers.go b/pkg/server/handlers.go index 36080577..e874918c 100644 --- a/pkg/server/handlers.go +++ b/pkg/server/handlers.go @@ -12,6 +12,7 @@ import ( "github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/fleetworkspace" "github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/globalrole" "github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/globalrolebinding" + "github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/nodedriver" "github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate" "github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/projectroletemplatebinding" "github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/roletemplate" @@ -39,8 +40,9 @@ func Validation(clients *clients.Clients) ([]admission.ValidatingAdmissionHandle crtbs := clusterroletemplatebinding.NewValidator(crtbResolver, clients.DefaultResolver, clients.RoleTemplateResolver) roleTemplates := roletemplate.NewValidator(clients.DefaultResolver, clients.RoleTemplateResolver, clients.K8s.AuthorizationV1().SubjectAccessReviews()) secrets := secret.NewValidator(clients.RBAC.Role().Cache(), clients.RBAC.RoleBinding().Cache()) + nodeDriver := nodedriver.NewValidator(clients.Management.Node().Cache(), clients.Dynamic) - handlers = append(handlers, psact, globalRoles, globalRoleBindings, prtbs, crtbs, roleTemplates, secrets) + handlers = append(handlers, psact, globalRoles, globalRoleBindings, prtbs, crtbs, roleTemplates, secrets, nodeDriver) } return handlers, nil } diff --git a/pkg/server/server.go b/pkg/server/server.go index 9d48b30c..8d847b7a 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -175,7 +175,7 @@ type secretHandler struct { } // sync updates the validating admission configuration whenever the TLS cert changes. -func (s *secretHandler) sync(key string, secret *corev1.Secret) (*corev1.Secret, error) { +func (s *secretHandler) sync(_ string, secret *corev1.Secret) (*corev1.Secret, error) { if secret == nil || secret.Name != caName || secret.Namespace != namespace || len(secret.Data[corev1.TLSCertKey]) == 0 { return nil, nil }