Skip to content

Commit

Permalink
[v0.5] s4: Fixes 227
Browse files Browse the repository at this point in the history
  • Loading branch information
pmatseykanets committed Jun 17, 2024
1 parent e05b359 commit d6e54d0
Show file tree
Hide file tree
Showing 13 changed files with 824 additions and 70 deletions.
150 changes: 81 additions & 69 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
Rancher Webhook
========
# Rancher Webhook

Rancher webhook is both a validating admission webhook and a mutating admission webhook for Kubernetes.

[Explanation of Webhooks in Kubernetes](
https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)

## Background

The Rancher Webhook is an instance of a Kubernetes admission controller.
Admission controllers are a standard Kubernetes mechanism to intercept requests to a cluster's
API server and perform validation or mutation of resources prior to their persistence in
Expand All @@ -26,62 +27,63 @@ It handles TLS certificates and the management of associated Secrets for secure

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`).
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. For example, `GlobalRole.md`.
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).

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).

``` golang
// WebhookHandler base interface for both ValidatingAdmissionHandler and MutatingAdmissionHandler.
// WebhookHandler is used for creating new http.HandlerFunc for each Webhook.
type WebhookHandler interface {

// GVR returns GroupVersionResource that the Webhook reviews.
// The returned GVR is used to define the route for accessing this webhook as well as creating the Webhooks Name.
// Thus the GVR returned must be unique from other WebhookHandlers of the same type e.g.(Mutating or Validating).
// If a WebhookHandler desires to monitor all resources in a group the Resource defined int he GVR should be "*".
// If a WebhookHandler desires to monitor a core type the Group can be left empty "".
GVR() schema.GroupVersionResource

// Operations returns list of operations that this WebhookHandler supports.
// Handlers will only be sent request with operations that are contained in the provided list.
Operations() []v1.OperationType

// Admit handles the webhook admission request sent to this webhook.
// The response returned by the WebhookHandler will be forwarded to the kube-api server.
// If the WebhookHandler can not accurately evaluate the request it should return an error.
Admit(*Request) (*admissionv1.AdmissionResponse, error)
// GVR returns GroupVersionResource that the Webhook reviews.
// The returned GVR is used to define the route for accessing this webhook as well as creating the Webhooks Name.
// Thus the GVR returned must be unique from other WebhookHandlers of the same type e.g.(Mutating or Validating).
// If a WebhookHandler desires to monitor all resources in a group the Resource defined int he GVR should be "*".
// If a WebhookHandler desires to monitor a core type the Group can be left empty "".
GVR() schema.GroupVersionResource

// Operations returns list of operations that this WebhookHandler supports.
// Handlers will only be sent request with operations that are contained in the provided list.
Operations() []v1.OperationType

// Admit handles the webhook admission request sent to this webhook.
// The response returned by the WebhookHandler will be forwarded to the kube-api server.
// If the WebhookHandler can not accurately evaluate the request it should return an error.
Admit(*Request) (*admissionv1.AdmissionResponse, error)
}

// ValidatingAdmissionHandler is a handler used for creating a ValidationAdmission Webhook.
type ValidatingAdmissionHandler interface {
WebhookHandler

// ValidatingWebhook returns a list of configurations to route to this handler.
//
// This functions allows ValidatingAdmissionHandler to perform modifications to the default configuration if needed.
// A default configuration can be made using NewDefaultValidatingWebhook(...)
// Most Webhooks implementing ValidatingWebhook will only return one configuration.
ValidatingWebhook(clientConfig v1.WebhookClientConfig) []v1.ValidatingWebhook
WebhookHandler

// ValidatingWebhook returns a list of configurations to route to this handler.
//
// This functions allows ValidatingAdmissionHandler to perform modifications to the default configuration if needed.
// A default configuration can be made using NewDefaultValidatingWebhook(...)
// Most Webhooks implementing ValidatingWebhook will only return one configuration.
ValidatingWebhook(clientConfig v1.WebhookClientConfig) []v1.ValidatingWebhook
}

// MutatingAdmissionHandler is a handler used for creating a MutatingAdmission Webhook.
type MutatingAdmissionHandler interface {
WebhookHandler

// MutatingWebhook returns a list of configurations to route to this handler.
//
// MutatingWebhook functions allows MutatingAdmissionHandler to perform modifications to the default configuration if needed.
// A default configuration can be made using NewDefaultMutatingWebhook(...)
// Most Webhooks implementing MutatingWebhook will only return one configuration.
MutatingWebhook(clientConfig v1.WebhookClientConfig) []v1.MutatingWebhook
WebhookHandler

// MutatingWebhook returns a list of configurations to route to this handler.
//
// MutatingWebhook functions allows MutatingAdmissionHandler to perform modifications to the default configuration if needed.
// A default configuration can be made using NewDefaultMutatingWebhook(...)
// Most Webhooks implementing MutatingWebhook will only return one configuration.
MutatingWebhook(clientConfig v1.WebhookClientConfig) []v1.MutatingWebhook
}


```

Any admission controller, as an app, consists of two main things:

1. The configuration which describes the resources and actions for which the webhook is active. This
configuration also references the Kubernetes service which directs traffic to the actual web-server
(this Webhook project) that does the work. The configuration exists as [ValidatingWebhookConfiguration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#validatingwebhookconfiguration-v1-admissionregistration-k8s-io)
Expand All @@ -92,8 +94,10 @@ _Note: both the ValidatingWebhookConfiguration and MutatingWebhookConfiguration
webhook startup, not beforehand._

All objects with custom validation logic exist in the `pkg/resources` package.
### Validation
Both Mutating and Validating webhooks can be used for basic validation of user input.

### Validation

Both Mutating and Validating webhooks can be used for basic validation of user input.
[A ValidatingAdmissionHandler should be used when validation is needed after all mutations are completed.](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#what-are-admission-webhooks)

For example, there is a
Expand All @@ -105,35 +109,39 @@ ensures that each rule of the GlobalRole has at least one verb. If it does not,
changes returns a response value with `Allowed` set to false.

```go
for _, rule := range newGR.Rules {
if len(rule.Verbs) == 0 {
return &admissionv1.AdmissionResponse{
Result: &metav1.Status{
Status: "Failure",
Message: "GlobalRole.Rules: PolicyRules must have at least one verb",
Reason: metav1.StatusReasonBadRequest,
Code: http.StatusBadRequest,
},
Allowed: false,
}, nil
}
}
for _, rule := range newGR.Rules {
if len(rule.Verbs) == 0 {
return &admissionv1.AdmissionResponse{
Result: &metav1.Status{
Status: "Failure",
Message: "GlobalRole.Rules: PolicyRules must have at least one verb",
Reason: metav1.StatusReasonBadRequest,
Code: http.StatusBadRequest,
},
Allowed: false,
}, nil
}
}
```

This logic is the main part of object inspection and admission control.

### Mutation

A MutatingAdmissionHandler should be used when the data being updated needs to be modified. All modifications must be recorded using a [JSONpatch](https://jsonpatch.com/). This can be done easily using the `pkg/patch` library for example the [MutatingAdmissionHandler for secrets](pkg/resources/core/v1/secret/mutator.go) add the creator's username as an annotation then creates a patch that is attached to the response.
```go
newSecret.Annotations[auth.CreatorIDAnn] = request.UserInfo.Username
response := &admissionv1.AdmissionResponse{}
if err := patch.CreatePatch(request.Object.Raw, newSecret, response); err != nil {
return nil, fmt.Errorf("failed to create patch: %w", err)
}
response.Allowed = true
return response, nil

```go
newSecret.Annotations[auth.CreatorIDAnn] = request.UserInfo.Username
response := &admissionv1.AdmissionResponse{}
if err := patch.CreatePatch(request.Object.Raw, newSecret, response); err != nil {
return nil, fmt.Errorf("failed to create patch: %w", err)
}
response.Allowed = true
return response, nil
```

### Creating a WebhookHandler

The `pkg/server` package is the main setup package of the Webhook server itself. The package defines the rules for resources and actions for which the Webhook will
be active. These are later brought to life as cluster-wide Kubernetes resources
(ValidatingWebhookConfiguration and MutatingWebhookConfiguration).
Expand All @@ -155,22 +163,26 @@ make

## Development


1. Get a new address that forwards to `https://localhost:9443` using ngrok.
```bash
ngrok http https://localhost:9443
```

```bash
ngrok http https://localhost:9443
```

2. Run the webhook with the given address and the kubeconfig for the cluster hosting Rancher.
``` bash
export KUBECONFIG=<rancher_kube_config>
export CATTLE_WEBHOOK_URL="https://<NGROK_URL>.ngrok.io"
./bin/webhook
```

``` bash
export KUBECONFIG=<rancher_kube_config>
export CATTLE_WEBHOOK_URL="https://<NGROK_URL>.ngrok.io"
./bin/webhook
```

After 15 seconds the webhook will update the `ValidatingWebhookConfiguration` and `MutatingWebhookConfiguration` in the Kubernetes cluster to point at the locally running instance.

> :warning: Kubernetes API server authentication will not work with ngrok.

## License

Copyright (c) 2019-2021 [Rancher Labs, Inc.](http://rancher.com)

Licensed under the Apache License, Version 2.0 (the "License");
Expand Down
42 changes: 42 additions & 0 deletions docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,48 @@ If `roletemplates.builtin` is true then all fields are immutable except:

RoleTemplate can not be deleted if they are referenced by other RoleTemplates via `roletemplates.roleTemplateNames` or by GlobalRoles via `globalRoles.inheritedClusterRoles`

## Setting

### Validation Checks

#### Invalid Fields - Create

When a Setting is created, the following checks take place:

- If set, `disable-inactive-user-after` must be zero or a positive duration (e.g. `240h`).
- If set, `delete-inactive-user-after` must be zero or a positive duration (e.g. `240h`).
- If set, `user-last-login-default` must be a date time according to RFC3339 (e.g. `2023-11-29T00:00:00Z`).
- If set, `user-retention-cron` must be a valid standard cron expression (e.g. `0 0 * * 0`).

#### Invalid Fields - Update

When a Setting is updated, the following checks take place:

- If set, `disable-inactive-user-after` must be zero or a positive duration (e.g. `240h`).
- If set, `delete-inactive-user-after` must be zero or a positive duration (e.g. `240h`).
- If set, `user-last-login-default` must be a date time according to RFC3339 (e.g. `2023-11-29T00:00:00Z`).
- If set, `user-retention-cron` must be a valid standard cron expression (e.g. `0 0 * * 1`).

## UserAttribute

### Validation Checks

#### Invalid Fields - Create

When a UserAttribute is created, the following checks take place:

- If set, `lastLogin` must be a valid date time according to RFC3339 (e.g. `2023-11-29T00:00:00Z`).
- If set, `disableAfter` must be zero or a positive duration (e.g. `240h`).
- If set, `deleteAfter` must be zero or a positive duration (e.g. `240h`).

#### Invalid Fields - Update

When a UserAttribute is updated, the following checks take place:

- If set, `lastLogin` must be a valid date time according to RFC3339 (e.g. `2023-11-29T00:00:00Z`).
- If set, `disableAfter` must be zero or a positive duration (e.g. `240h`).
- If set, `deleteAfter` must be zero or a positive duration (e.g. `240h`).

# rbac.authorization.k8s.io/v1

## Role
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ require (
github.com/rancher/rancher/pkg/apis v0.0.0-20240328110445-91a4620d7e49
github.com/rancher/rke v1.5.7-rc2
github.com/rancher/wrangler/v2 v2.1.4
github.com/robfig/cron v1.2.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ github.com/rancher/wrangler v1.1.1 h1:wmqUwqc2M7ADfXnBCJTFkTB5ZREWpD78rnZMzmxwMv
github.com/rancher/wrangler v1.1.1/go.mod h1:ioVbKupzcBOdzsl55MvEDN0R1wdGggj8iNCYGFI5JvM=
github.com/rancher/wrangler/v2 v2.1.4 h1:ohov0i6A9dJHHO6sjfsH4Dqv93ZTdm5lIJVJdPzVdQc=
github.com/rancher/wrangler/v2 v2.1.4/go.mod h1:af5OaGU/COgreQh1mRbKiUI64draT2NN34uk+PALFY8=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
Expand Down
1 change: 1 addition & 0 deletions pkg/codegen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func main() {
&v3.ProjectRoleTemplateBinding{},
&v3.NodeDriver{},
&v3.Project{},
&v3.Setting{},
},
},
"provisioning.cattle.io": {
Expand Down
53 changes: 53 additions & 0 deletions pkg/generated/objects/management.cattle.io/v3/objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -590,3 +590,56 @@ func ProjectFromRequest(request *admissionv1.AdmissionRequest) (*v3.Project, err

return object, nil
}

// SettingOldAndNewFromRequest gets the old and new Setting objects, respectively, from the webhook request.
// If the request is a Delete operation, then the new object is the zero value for Setting.
// Similarly, if the request is a Create operation, then the old object is the zero value for Setting.
func SettingOldAndNewFromRequest(request *admissionv1.AdmissionRequest) (*v3.Setting, *v3.Setting, error) {
if request == nil {
return nil, nil, fmt.Errorf("nil request")
}

object := &v3.Setting{}
oldObject := &v3.Setting{}

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
}

// SettingFromRequest returns a Setting 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 SettingFromRequest(request *admissionv1.AdmissionRequest) (*v3.Setting, error) {
if request == nil {
return nil, fmt.Errorf("nil request")
}

object := &v3.Setting{}
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
}
19 changes: 19 additions & 0 deletions pkg/resources/management.cattle.io/v3/setting/Setting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Validation Checks

### Invalid Fields - Create

When a Setting is created, the following checks take place:

- If set, `disable-inactive-user-after` must be zero or a positive duration (e.g. `240h`).
- If set, `delete-inactive-user-after` must be zero or a positive duration (e.g. `240h`).
- If set, `user-last-login-default` must be a date time according to RFC3339 (e.g. `2023-11-29T00:00:00Z`).
- If set, `user-retention-cron` must be a valid standard cron expression (e.g. `0 0 * * 0`).

### Invalid Fields - Update

When a Setting is updated, the following checks take place:

- If set, `disable-inactive-user-after` must be zero or a positive duration (e.g. `240h`).
- If set, `delete-inactive-user-after` must be zero or a positive duration (e.g. `240h`).
- If set, `user-last-login-default` must be a date time according to RFC3339 (e.g. `2023-11-29T00:00:00Z`).
- If set, `user-retention-cron` must be a valid standard cron expression (e.g. `0 0 * * 1`).
Loading

0 comments on commit d6e54d0

Please sign in to comment.