Skip to content

Commit

Permalink
Merge pull request #256 from rancher/dev/v0.3
Browse files Browse the repository at this point in the history
Dev/v0.3
  • Loading branch information
KevinJoiner authored Jul 10, 2023
2 parents 2e89c65 + c977cf2 commit 611c2ef
Show file tree
Hide file tree
Showing 45 changed files with 1,326 additions and 76 deletions.
4 changes: 2 additions & 2 deletions Dockerfile.dapper
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
129 changes: 129 additions & 0 deletions docs.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion package/Dockerfile
Original file line number Diff line number Diff line change
@@ -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

Expand Down
6 changes: 3 additions & 3 deletions pkg/admission/admission_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand All @@ -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
}
4 changes: 4 additions & 0 deletions pkg/codegen/cleanup/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
142 changes: 142 additions & 0 deletions pkg/codegen/docs.go
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 6 additions & 0 deletions pkg/codegen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -36,6 +40,7 @@ func main() {
v3.RoleTemplate{},
v3.ClusterRoleTemplateBinding{},
v3.ProjectRoleTemplateBinding{},
v3.Node{},
},
},
"provisioning.cattle.io": {
Expand All @@ -59,6 +64,7 @@ func main() {
&v3.GlobalRoleBinding{},
&v3.RoleTemplate{},
&v3.ProjectRoleTemplateBinding{},
&v3.NodeDriver{},
},
},
"provisioning.cattle.io": {
Expand Down
Loading

0 comments on commit 611c2ef

Please sign in to comment.