From d3a76653ad29a695062c0b19d68b40ea142b12e8 Mon Sep 17 00:00:00 2001 From: "renovate-rancher[bot]" <119870437+renovate-rancher[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 04:39:54 +0000 Subject: [PATCH 1/8] Update dependency golangci/golangci-lint to v1.52.2 --- Dockerfile.dapper | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.dapper b/Dockerfile.dapper index f257782a..f3a742eb 100644 --- a/Dockerfile.dapper +++ b/Dockerfile.dapper @@ -5,7 +5,7 @@ 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 RUN curl -sL https://get.helm.sh/helm-${HELM_VERSION}-linux-${ARCH}.tar.gz | tar xvzf - -C /usr/local/bin --strip-components=1 From f36421b8a73d9bdb3b4345c039aab0b764c3ba31 Mon Sep 17 00:00:00 2001 From: Kevin Joiner Date: Tue, 25 Apr 2023 15:29:07 -0400 Subject: [PATCH 2/8] golangci-lint cleanup --- pkg/mocks/RoleTemplateCache.go | 39 ++----------------- pkg/resolvers/crtbResolver.go | 2 +- pkg/resolvers/prtbResolver.go | 2 +- pkg/resolvers/resolvers.go | 2 +- .../validator.go | 6 +-- .../validator_test.go | 16 ++++---- pkg/server/server.go | 2 +- 7 files changed, 17 insertions(+), 52 deletions(-) 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/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate/validator.go b/pkg/resources/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate/validator.go index a4aa7756..0d1e7c03 100644 --- a/pkg/resources/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate/validator.go +++ b/pkg/resources/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate/validator.go @@ -214,11 +214,7 @@ func (v *Validator) validateConfiguration(configurationTemplate *mgmtv3.PodSecur 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 48fa2455..3e335f83 100644 --- a/pkg/resources/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate/validator_test.go +++ b/pkg/resources/management.cattle.io/v3/podsecurityadmissionconfigurationtemplate/validator_test.go @@ -594,22 +594,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{ @@ -628,22 +628,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/server/server.go b/pkg/server/server.go index f4b5dda0..026fa369 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 } From 9c124a4e10c0d21ac6bc0a176ba9aa5af8c62d43 Mon Sep 17 00:00:00 2001 From: Michael Bolot Date: Fri, 28 Apr 2023 15:53:47 -0500 Subject: [PATCH 3/8] Adding docs generation and example docs files Adds logic to auto-generate a documentation directory and provides two docs files --- README.md | 8 ++ docs.md | 24 ++++ main.go | 2 +- pkg/codegen/cleanup/main.go | 4 + pkg/codegen/docs.go | 117 ++++++++++++++++++ pkg/codegen/main.go | 4 + .../v3/globalrole/GlobalRole.md | 8 ++ .../v3/roletemplate/RoleTemplate.md | 16 +++ 8 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 docs.md create mode 100644 pkg/codegen/docs.go create mode 100644 pkg/resources/management.cattle.io/v3/globalrole/GlobalRole.md create mode 100644 pkg/resources/management.cattle.io/v3/roletemplate/RoleTemplate.md diff --git a/README.md b/README.md index bc7572fb..eaf6e0d3 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 CRDs that are validated 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 CRD'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..73aea836 --- /dev/null +++ b/docs.md @@ -0,0 +1,24 @@ +# management.cattle.io/v3 + +## GlobalRole + +### Validation Checks +Note: all checks are bypassed if the GlobalRole is being deleted + +#### Escalation Prevention +Users can only change GlobalRoles which have less permissions than they do. This is to prevents privilege escalation. + +## 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 peritted. 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 which have less permissions than they do. This prevents privilege escalation. + diff --git a/main.go b/main.go index 5b90bbdc..4b12ab75 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/main.go pkg/codegen/template.go pkg/codegen/docs.go package main import ( 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..9b0fc742 --- /dev/null +++ b/pkg/codegen/docs.go @@ -0,0 +1,117 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "golang.org/x/exp/slices" +) + +// 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) error { + outputFile, err := os.OpenFile(outputFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + 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 + _, err = fmt.Fprintf(outputFile, "# %s/%s \n \n", 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, "## %s \n\n", docFile.resource) + if err != nil { + return fmt.Errorf("unable to write resource header for %s: %w", docFile.resource, err) + } + + lines := strings.Split(string(docFile.content), "\n") + for i, line := range lines { + newLine := line + if i < len(lines)-1 { + // last line doesn't need a newLine re-added + newLine += "\n" + } + if strings.HasPrefix(line, "#") { + // this line is a markdown header. Since the group header is the top-level indent, indent this down one line + newLine = "#" + line + } + _, err := outputFile.WriteString(newLine) + 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...) + } + if filepath.Ext(entry.Name()) == docFileExtension { + 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) + } + var newDir, resource, version, group string + 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) + } + docFiles = append(docFiles, docFile{ + content: content, + resource: resource, + group: group, + version: 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 { + return true + } else if a.group == b.group { + return a.resource < b.resource + } + return false + }) + + return docFiles, nil +} diff --git a/pkg/codegen/main.go b/pkg/codegen/main.go index a5c93cd9..c187a58c 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", 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..7c04c726 --- /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 which have less permissions than they do. This is to prevents privilege escalation. + 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..b500a949 --- /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 peritted. 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 which have less permissions than they do. This prevents privilege escalation. + From 140e07e45b9dbd5e079a76d61b6df49dc4f0551d Mon Sep 17 00:00:00 2001 From: "renovate-rancher[bot]" <119870437+renovate-rancher[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 04:38:56 +0000 Subject: [PATCH 4/8] Update registry.suse.com/bci/bci-micro Docker tag to v15.4.20.1 --- package/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a6c9cc29782e3ce18bc833ea713a9b47f82c4c7e Mon Sep 17 00:00:00 2001 From: Michael Bolot Date: Wed, 10 May 2023 16:20:18 -0500 Subject: [PATCH 5/8] Adding docs for some resources --- README.md | 4 +- docs.md | 103 +++++++++++++++++- go.mod | 2 +- main.go | 2 +- pkg/codegen/docs.go | 101 ++++++++++------- .../management.cattle.io/factory.go | 2 +- .../management.cattle.io/interface.go | 2 +- .../management.cattle.io/v3/cluster.go | 2 +- .../v3/clusterroletemplatebinding.go | 2 +- .../management.cattle.io/v3/globalrole.go | 2 +- .../management.cattle.io/v3/interface.go | 2 +- ...dsecurityadmissionconfigurationtemplate.go | 2 +- .../v3/projectroletemplatebinding.go | 2 +- .../management.cattle.io/v3/roletemplate.go | 2 +- .../provisioning.cattle.io/factory.go | 2 +- .../provisioning.cattle.io/interface.go | 2 +- .../provisioning.cattle.io/v1/cluster.go | 2 +- .../provisioning.cattle.io/v1/interface.go | 2 +- pkg/resources/core/v1/namespace/Namespace.md | 10 ++ .../ClusterRoleTemplateBinding.md | 28 +++++ .../v3/globalrole/GlobalRole.md | 2 +- .../v3/globalrolebinding/GlobalRoleBinding.md | 11 ++ .../ProjectRoleTemplateBinding.md | 28 +++++ .../v3/roletemplate/RoleTemplate.md | 4 +- 24 files changed, 259 insertions(+), 62 deletions(-) create mode 100644 pkg/resources/core/v1/namespace/Namespace.md create mode 100644 pkg/resources/management.cattle.io/v3/clusterroletemplatebinding/ClusterRoleTemplateBinding.md create mode 100644 pkg/resources/management.cattle.io/v3/globalrolebinding/GlobalRoleBinding.md create mode 100644 pkg/resources/management.cattle.io/v3/projectroletemplatebinding/ProjectRoleTemplateBinding.md diff --git a/README.md b/README.md index eaf6e0d3..f2aec9d5 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,10 @@ It handles TLS certificates and the management of associated Secrets for secure ## Docs -Documentation on each of the CRDs that are validated 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. +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 CRD's name. +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 diff --git a/docs.md b/docs.md index 73aea836..3adb35c1 100644 --- a/docs.md +++ b/docs.md @@ -1,24 +1,119 @@ +# 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 which have less permissions than they do. This is to prevents privilege escalation. + +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). + +## 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 peritted. 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. + +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 which have less permissions than they do. This prevents privilege escalation. +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 4b12ab75..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 pkg/codegen/docs.go +//go:generate go run ./pkg/codegen package main import ( diff --git a/pkg/codegen/docs.go b/pkg/codegen/docs.go index 9b0fc742..d8b9e4ac 100644 --- a/pkg/codegen/docs.go +++ b/pkg/codegen/docs.go @@ -1,12 +1,16 @@ 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 @@ -19,8 +23,18 @@ type docFile struct { version string } -func generateDocs(resourcesBaseDir, outputFilePath string) error { +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 } @@ -33,34 +47,37 @@ func generateDocs(resourcesBaseDir, outputFilePath string) error { newGroup := docFile.group if newGroup != currentGroup { // our group has changed, output a new group header - _, err = fmt.Fprintf(outputFile, "# %s/%s \n \n", docFile.group, docFile.version) + 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, "## %s \n\n", docFile.resource) + _, 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) } - - lines := strings.Split(string(docFile.content), "\n") - for i, line := range lines { - newLine := line - if i < len(lines)-1 { - // last line doesn't need a newLine re-added - newLine += "\n" - } - if strings.HasPrefix(line, "#") { + 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 - newLine = "#" + line + line = append([]byte{'#'}, line...) } - _, err := outputFile.WriteString(newLine) + 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 } @@ -81,36 +98,44 @@ func getDocFiles(baseDir string) ([]docFile, error) { return nil, err } docFiles = append(docFiles, subDocFiles...) + continue } - if filepath.Ext(entry.Name()) == docFileExtension { - 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) - } - var newDir, resource, version, group string - 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) - } - docFiles = append(docFiles, docFile{ - content: content, - resource: resource, - group: group, - version: version, - }) + 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 { - return true - } else if a.group == b.group { - return a.resource < b.resource + if a.group == b.group { + if a.resource == b.resource { + return a.version < b.version + } + return a.resource == b.resource } - return false + return a.group < b.group }) return docFiles, nil 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..8dc7242c 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 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/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/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 index 7c04c726..46fc5ab4 100644 --- a/pkg/resources/management.cattle.io/v3/globalrole/GlobalRole.md +++ b/pkg/resources/management.cattle.io/v3/globalrole/GlobalRole.md @@ -4,5 +4,5 @@ Note: all checks are bypassed if the GlobalRole is being deleted ### Escalation Prevention -Users can only change GlobalRoles which have less permissions than they do. This is to prevents privilege escalation. +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/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 index b500a949..c820fe8c 100644 --- a/pkg/resources/management.cattle.io/v3/roletemplate/RoleTemplate.md +++ b/pkg/resources/management.cattle.io/v3/roletemplate/RoleTemplate.md @@ -8,9 +8,9 @@ Circular references to webhooks (a inherits b, b inherits a) are not allowed. Mo ### Rules Without Verbs -Rules without verbs are not peritted. 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. +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 which have less permissions than they do. This prevents privilege escalation. +Users can only change RoleTemplates with rights less than or equal to those they currently possess. This prevents privilege escalation. From 00f3e9bd407a54c9bc3cf9844a8bb67bda0616c7 Mon Sep 17 00:00:00 2001 From: "renovate-rancher[bot]" <119870437+renovate-rancher[bot]@users.noreply.github.com> Date: Thu, 15 Jun 2023 04:39:06 +0000 Subject: [PATCH 6/8] Update dependency helm/helm to v3.12.1 --- Dockerfile.dapper | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.dapper b/Dockerfile.dapper index f257782a..134da97a 100644 --- a/Dockerfile.dapper +++ b/Dockerfile.dapper @@ -7,7 +7,7 @@ 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; \ 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 From 2c9d0c1c44291261e763b59236bc6bb1c92c5128 Mon Sep 17 00:00:00 2001 From: Jacob Lindgren Date: Tue, 27 Jun 2023 15:40:23 -0500 Subject: [PATCH 7/8] fix warnings found by golangci-lint --- pkg/admission/admission_test.go | 6 +++--- pkg/resources/core/v1/secret/mutator.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) 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/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) From 0a8256b1d0b25bef83c782c35a68765e942767ad Mon Sep 17 00:00:00 2001 From: Jacob Lindgren Date: Thu, 22 Jun 2023 12:32:09 -0500 Subject: [PATCH 8/8] Add Validation on NodeDriver update/delete to prevent machine deletion --- docs.md | 10 + pkg/codegen/main.go | 2 + pkg/fakes/NodeDriverCache.go | 94 +++++ pkg/fakes/generator.go | 1 + .../management.cattle.io/v3/interface.go | 4 + .../management.cattle.io/v3/node.go | 376 ++++++++++++++++++ .../management.cattle.io/v3/objects.go | 53 +++ .../v3/nodedriver/NodeDriver.md | 7 + .../v3/nodedriver/validator.go | 153 +++++++ .../v3/nodedriver/validator_test.go | 205 ++++++++++ pkg/server/handlers.go | 4 +- 11 files changed, 908 insertions(+), 1 deletion(-) create mode 100644 pkg/fakes/NodeDriverCache.go create mode 100644 pkg/generated/controllers/management.cattle.io/v3/node.go create mode 100644 pkg/resources/management.cattle.io/v3/nodedriver/NodeDriver.md create mode 100644 pkg/resources/management.cattle.io/v3/nodedriver/validator.go create mode 100644 pkg/resources/management.cattle.io/v3/nodedriver/validator_test.go diff --git a/docs.md b/docs.md index 3adb35c1..40c3cfa8 100644 --- a/docs.md +++ b/docs.md @@ -69,6 +69,16 @@ Users can only create/update GlobalRoleBindings with rights less than or equal t 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 diff --git a/pkg/codegen/main.go b/pkg/codegen/main.go index c187a58c..ae93a53c 100644 --- a/pkg/codegen/main.go +++ b/pkg/codegen/main.go @@ -40,6 +40,7 @@ func main() { v3.RoleTemplate{}, v3.ClusterRoleTemplateBinding{}, v3.ProjectRoleTemplateBinding{}, + v3.Node{}, }, }, "provisioning.cattle.io": { @@ -63,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/v3/interface.go b/pkg/generated/controllers/management.cattle.io/v3/interface.go index 8dc7242c..e7f6240d 100644 --- a/pkg/generated/controllers/management.cattle.io/v3/interface.go +++ b/pkg/generated/controllers/management.cattle.io/v3/interface.go @@ -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/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/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/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 }