Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate LastUsedAt for Token and ClusterAuthToken #480

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,22 @@ When a Setting is updated, the following checks take place:
have a status condition `AgentTlsStrictCheck` set to `True`, unless the new setting has an overriding
annotation `cattle.io/force=true`.

## Token

### Validation Checks

#### Invalid Fields - Create

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

- If set, `lastUsedAt` must be a valid date time according to RFC3339 (e.g. `2023-11-29T00:00:00Z`).

#### Invalid Fields - Update

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

- If set, `lastUsedAt` must be a valid date time according to RFC3339 (e.g. `2023-11-29T00:00:00Z`).

## UserAttribute

### Validation Checks
Expand Down
13 changes: 13 additions & 0 deletions pkg/resources/management.cattle.io/v3/token/Token.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Validation Checks

### Invalid Fields - Create

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

- If set, `lastUsedAt` must be a valid date time according to RFC3339 (e.g. `2023-11-29T00:00:00Z`).

### Invalid Fields - Update

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

- If set, `lastUsedAt` must be a valid date time according to RFC3339 (e.g. `2023-11-29T00:00:00Z`).
93 changes: 93 additions & 0 deletions pkg/resources/management.cattle.io/v3/token/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package token

import (
"encoding/json"
"fmt"
"time"

"github.com/rancher/webhook/pkg/admission"
admissionv1 "k8s.io/api/admission/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/trace"
)

var gvr = schema.GroupVersionResource{
Group: "management.cattle.io",
Version: "v3",
Resource: "tokens",
}

// Validator validates tokens.
type Validator struct {
admitter admitter
}

// NewValidator returns a new Validator instance.
func NewValidator() *Validator {
return &Validator{
admitter: admitter{},
}
}

// GVR returns the GroupVersionResource.
func (v *Validator) GVR() schema.GroupVersionResource {
return gvr
}

// Operations returns list of operations handled by the validator.
func (v *Validator) Operations() []admissionregistrationv1.OperationType {
return []admissionregistrationv1.OperationType{admissionregistrationv1.Update, admissionregistrationv1.Create}
}

// ValidatingWebhook returns the ValidatingWebhook.
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.
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.admitter}
}

type admitter struct{}

// Admit handles the webhook admission requests.
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
listTrace := trace.New("tokenValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)

if request.Operation == admissionv1.Create || request.Operation == admissionv1.Update {
err := a.validateTokenFields(request)
if err != nil {
return admission.ResponseBadRequest(err.Error()), nil
}
}

return admission.ResponseAllowed(), nil
}

// PartialToken represents raw values of Token fields.
type PartialToken struct {
LastUsedAt *string `json:"lastUsedAt"`
}

func (a *admitter) validateTokenFields(request *admission.Request) error {
var tok PartialToken

err := json.Unmarshal(request.Object.Raw, &tok)
if err != nil {
return fmt.Errorf("failed to get PartialToken from request: %w", err)
}

if tok.LastUsedAt != nil {
if _, err = time.Parse(time.RFC3339, *tok.LastUsedAt); err != nil {
return field.TypeInvalid(field.NewPath("lastUsedAt"), tok.LastUsedAt, err.Error())
}
}

return nil
}
135 changes: 135 additions & 0 deletions pkg/resources/management.cattle.io/v3/token/validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package token_test

import (
"context"
"encoding/json"
"testing"
"time"

"github.com/rancher/webhook/pkg/admission"
"github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/token"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
v1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/pointer"
)

type TokenFieldsSuite struct {
suite.Suite
}

func TestTokenFieldsValidation(t *testing.T) {
t.Parallel()
suite.Run(t, new(TokenFieldsSuite))
}

var (
gvk = metav1.GroupVersionKind{Group: "management.cattle.io", Version: "v3", Kind: "Token"}
gvr = metav1.GroupVersionResource{Group: "management.cattle.io", Version: "v3", Resource: "tokens"}
)

type tokenFieldsTest struct {
lastUsedAt *string
allowed bool
}

func (t *tokenFieldsTest) name() string {
return pointer.StringDeref(t.lastUsedAt, "nil")
}

func (t *tokenFieldsTest) toToken() ([]byte, error) {
return json.Marshal(token.PartialToken{
LastUsedAt: t.lastUsedAt,
})
}

var tokenFieldsTests = []tokenFieldsTest{
{
allowed: true,
},
{
lastUsedAt: pointer.String(time.Now().Format(time.RFC3339)),
allowed: true,
},
{
lastUsedAt: pointer.String("2024-03-25T21:2:45Z"), // Not a valid RFC3339 time.
},
{
lastUsedAt: pointer.String("1w"),
},
{
lastUsedAt: pointer.String("1d"),
},
{
lastUsedAt: pointer.String("-1h"),
},
{
lastUsedAt: pointer.String(""),
},
}

func (s *TokenFieldsSuite) TestValidateOnUpdate() {
s.validate(v1.Update)
}

func (s *TokenFieldsSuite) TestValidateOnCreate() {
s.validate(v1.Create)
}

func (s *TokenFieldsSuite) TestDontValidateOnDelete() {
// Make sure that Token can be deleted without enforcing validation of user token fields.
alwaysAllow := true
s.validate(v1.Delete, alwaysAllow)
}

func (s *TokenFieldsSuite) validate(op v1.Operation, allowed ...bool) {
JonCrowther marked this conversation as resolved.
Show resolved Hide resolved
admitter := s.setup()

for _, test := range tokenFieldsTests {
test := test
s.Run(test.name(), func() {
t := s.T()
t.Parallel()

objRaw, err := test.toToken()
assert.NoError(t, err, "failed to marshal PartialToken")

resp, err := admitter.Admit(newRequest(op, objRaw))
if assert.NoError(t, err, "Admit failed") {
wantAllowed := test.allowed
if len(allowed) > 0 {
wantAllowed = allowed[0] // Apply the override.
}

assert.Equalf(t, wantAllowed, resp.Allowed, "expected allowed %v got %v message=%v", test.allowed, resp.Allowed, resp.Result)
}
})
}
}

func (s *TokenFieldsSuite) setup() admission.Admitter {
validator := token.NewValidator()
s.Len(validator.Admitters(), 1, "expected 1 admitter")

return validator.Admitters()[0]
}

func newRequest(op v1.Operation, obj []byte) *admission.Request {
return &admission.Request{
AdmissionRequest: v1.AdmissionRequest{
UID: "1",
Kind: gvk,
Resource: gvr,
RequestKind: &gvk,
RequestResource: &gvr,
Operation: op,
UserInfo: authenticationv1.UserInfo{Username: "foo", UID: ""},
Object: runtime.RawExtension{Raw: obj},
OldObject: runtime.RawExtension{Raw: []byte("{}")},
},
Context: context.Background(),
}
}
4 changes: 3 additions & 1 deletion pkg/server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/projectroletemplatebinding"
"github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/roletemplate"
"github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/setting"
"github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/token"
"github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/userattribute"
provisioningCluster "github.com/rancher/webhook/pkg/resources/provisioning.cattle.io/v1/cluster"
"github.com/rancher/webhook/pkg/resources/rbac.authorization.k8s.io/v1/clusterrole"
Expand Down Expand Up @@ -55,11 +56,12 @@ func Validation(clients *clients.Clients) ([]admission.ValidatingAdmissionHandle
roles := role.NewValidator()
rolebindings := rolebinding.NewValidator()
setting := setting.NewValidator(clients.Management.Cluster().Cache())
token := token.NewValidator()
userAttribute := userattribute.NewValidator()
clusterRoles := clusterrole.NewValidator()
clusterRoleBindings := clusterrolebinding.NewValidator()

handlers = append(handlers, psact, globalRoles, globalRoleBindings, prtbs, crtbs, roleTemplates, secrets, nodeDriver, projects, roles, rolebindings, clusterRoles, clusterRoleBindings, clusterProxyConfigs, userAttribute, setting)
handlers = append(handlers, psact, globalRoles, globalRoleBindings, prtbs, crtbs, roleTemplates, secrets, nodeDriver, projects, roles, rolebindings, clusterRoles, clusterRoleBindings, clusterProxyConfigs, userAttribute, setting, token)
}
return handlers, nil
}
Expand Down
Loading