diff --git a/api/v1alpha1/authorization_types.go b/api/v1alpha1/authorization_types.go new file mode 100644 index 000000000000..20b9104bf665 --- /dev/null +++ b/api/v1alpha1/authorization_types.go @@ -0,0 +1,54 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package v1alpha1 + +// Authorization defines the authorization configuration. +type Authorization struct { + // Rules contains all the authorization rules. + // + // +kubebuilder:validation:MinItems=1 + Rules []Rule `json:"rules,omitempty"` +} + +// Rule defines the single authorization rule. +type Rule struct { + // Subjects contains the subject configuration. + // If empty, all subjects are included. + // + // +optional + Subjects []Subject `json:"subjects,omitempty"` + + // Permissions contains allowed HTTP methods. + // If empty, all methods are matching. + // + // +optional + Permissions []string `json:"permissions,omitempty"` + + // Action defines the action to be taken if the rule matches. + Action RuleActionType `json:"action"` +} + +// Subject contains the subject configuration. +type Subject struct { + // ClientCIDR contains client cidr configuration. + // Valid examples are "192.168.1.0/24" or "2001:db8::/64" + // + // +optional + ClientCIDR *string `json:"clientCIDR,omitempty"` +} + +// RuleActionType specifies the types of authorization rule action. +// +kubebuilder:validation:Enum=Allow;Deny;Log +type RuleActionType string + +const ( + // Allow is the action to allow the request. + Allow RuleActionType = "Allow" + // Deny is the action to deny the request. + Deny RuleActionType = "Deny" + // Log is the action to log the request. + Log RuleActionType = "Log" +) diff --git a/api/v1alpha1/securitypolicy_types.go b/api/v1alpha1/securitypolicy_types.go index 85c0b21892dc..74e3b2ca5aac 100644 --- a/api/v1alpha1/securitypolicy_types.go +++ b/api/v1alpha1/securitypolicy_types.go @@ -69,6 +69,11 @@ type SecurityPolicySpec struct { // // +optional ExtAuth *ExtAuth `json:"extAuth,omitempty"` + + // Authorization defines the authorization configuration. + // + // +optional + Authorization *Authorization `json:"authorization,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/validation/securitypolicy_validate.go b/api/v1alpha1/validation/securitypolicy_validate.go index 628d3f801730..5f40b252e572 100644 --- a/api/v1alpha1/validation/securitypolicy_validate.go +++ b/api/v1alpha1/validation/securitypolicy_validate.go @@ -8,6 +8,7 @@ package validation import ( "errors" "fmt" + "net" "net/mail" "net/url" @@ -24,7 +25,7 @@ func ValidateSecurityPolicy(policy *egv1a1.SecurityPolicy) error { return errors.New("policy is nil") } if err := validateSecurityPolicySpec(&policy.Spec); err != nil { - errs = append(errs, errors.New("policy is nil")) + errs = append(errs, err) } return utilerrors.NewAggregate(errs) @@ -42,6 +43,8 @@ func validateSecurityPolicySpec(spec *egv1a1.SecurityPolicySpec) error { sum++ case spec.JWT != nil: sum++ + case spec.Authorization != nil: + sum++ } if sum == 0 { errs = append(errs, errors.New("no security policy is specified")) @@ -52,13 +55,40 @@ func validateSecurityPolicySpec(spec *egv1a1.SecurityPolicySpec) error { return utilerrors.NewAggregate(errs) } - if err := ValidateJWTProvider(spec.JWT.Providers); err != nil { + if spec.JWT != nil { + if err := ValidateJWTProvider(spec.JWT.Providers); err != nil { + errs = append(errs, err) + } + } + + if err := ValidateAuthorization(spec.Authorization); err != nil { errs = append(errs, err) } return utilerrors.NewAggregate(errs) } +// ValidateAuthorization validates the provided Authorisation configuration. +func ValidateAuthorization(as *egv1a1.Authorization) error { + var errs []error + if as == nil { + return nil + } + + for _, rule := range as.Rules { + for _, subject := range rule.Subjects { + if subject.ClientCIDR != nil { + _, _, err := net.ParseCIDR(*subject.ClientCIDR) + if err != nil { + errs = append(errs, fmt.Errorf("invalid CIDR: %s", *subject.ClientCIDR)) + } + } + } + } + + return utilerrors.NewAggregate(errs) +} + // ValidateJWTProvider validates the provided JWT authentication configuration. func ValidateJWTProvider(providers []egv1a1.JWTProvider) error { var errs []error diff --git a/api/v1alpha1/validation/securitypolicy_validate_test.go b/api/v1alpha1/validation/securitypolicy_validate_test.go index 489c7644f8b1..8bf286ac8bde 100644 --- a/api/v1alpha1/validation/securitypolicy_validate_test.go +++ b/api/v1alpha1/validation/securitypolicy_validate_test.go @@ -8,12 +8,16 @@ package validation import ( "testing" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" ) +func asPtr(s string) *string { + return &s +} + func TestValidateSecurityPolicy(t *testing.T) { testCases := []struct { name string @@ -463,6 +467,118 @@ func TestValidateSecurityPolicy(t *testing.T) { }, expected: true, }, + { + name: "authorisation with valid ipv4 cidr", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + Authorization: &egv1a1.Authorization{ + Rules: []egv1a1.Rule{ + { + Subjects: []egv1a1.Subject{ + { + ClientCIDR: asPtr("192.168.1.0/24"), + }, + }, + Action: egv1a1.Allow, + }, + }, + }, + }, + }, + expected: true, + }, + { + name: "authorisation with valid ipv6 cidr", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + Authorization: &egv1a1.Authorization{ + Rules: []egv1a1.Rule{ + { + Subjects: []egv1a1.Subject{ + { + ClientCIDR: asPtr("2001:db8::/64"), + }, + }, + Action: egv1a1.Allow, + }, + }, + }, + }, + }, + expected: true, + }, + { + name: "authorisation with invalid ipv4 cidr", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + Authorization: &egv1a1.Authorization{ + Rules: []egv1a1.Rule{ + { + Subjects: []egv1a1.Subject{ + { + ClientCIDR: asPtr("192.168.1.001/24"), + }, + }, + Action: egv1a1.Allow, + }, + }, + }, + }, + }, + expected: false, + }, + { + name: "authorisation with invalid ipv6 cidr", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + Authorization: &egv1a1.Authorization{ + Rules: []egv1a1.Rule{ + { + Subjects: []egv1a1.Subject{ + { + ClientCIDR: asPtr("2001:dffoob8::/64"), + }, + }, + Action: egv1a1.Allow, + }, + }, + }, + }, + }, + expected: false, + }, } for i := range testCases { @@ -470,9 +586,9 @@ func TestValidateSecurityPolicy(t *testing.T) { t.Run(tc.name, func(t *testing.T) { err := ValidateSecurityPolicy(tc.policy) if tc.expected { - require.NoError(t, err) + assert.NoError(t, err) } else { - require.Error(t, err) + assert.Error(t, err) } }) } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c3d53f443bae..05c2d611acc4 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -151,6 +151,28 @@ func (in *ActiveHealthCheckPayload) DeepCopy() *ActiveHealthCheckPayload { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Authorization) DeepCopyInto(out *Authorization) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]Rule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authorization. +func (in *Authorization) DeepCopy() *Authorization { + if in == nil { + return nil + } + out := new(Authorization) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackOffPolicy) DeepCopyInto(out *BackOffPolicy) { *out = *in @@ -3806,6 +3828,33 @@ func (in *RetryOn) DeepCopy() *RetryOn { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Rule) DeepCopyInto(out *Rule) { + *out = *in + if in.Subjects != nil { + in, out := &in.Subjects, &out.Subjects + *out = make([]Subject, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Permissions != nil { + in, out := &in.Permissions, &out.Permissions + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Rule. +func (in *Rule) DeepCopy() *Rule { + if in == nil { + return nil + } + out := new(Rule) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecurityPolicy) DeepCopyInto(out *SecurityPolicy) { *out = *in @@ -3894,6 +3943,11 @@ func (in *SecurityPolicySpec) DeepCopyInto(out *SecurityPolicySpec) { *out = new(ExtAuth) (*in).DeepCopyInto(*out) } + if in.Authorization != nil { + in, out := &in.Authorization, &out.Authorization + *out = new(Authorization) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityPolicySpec. @@ -3991,6 +4045,26 @@ func (in *StringMatch) DeepCopy() *StringMatch { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Subject) DeepCopyInto(out *Subject) { + *out = *in + if in.ClientCIDR != nil { + in, out := &in.ClientCIDR, &out.ClientCIDR + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Subject. +func (in *Subject) DeepCopy() *Subject { + if in == nil { + return nil + } + out := new(Subject) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TCPActiveHealthChecker) DeepCopyInto(out *TCPActiveHealthChecker) { *out = *in diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml index 2758a9c2524d..cd0ed80014d8 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -49,6 +49,45 @@ spec: spec: description: Spec defines the desired state of SecurityPolicy. properties: + authorization: + description: Authorization defines the authorization configuration. + properties: + rules: + description: Rules contains all the authorization rules. If rules + contains at least one Allow rule and none of them matches the + action for the request is deny. If rules contains at least one + Deny rule and none of them matches the action for the request + is allow. If rules contains Allow and Deny, and the request + matches both, `Deny` takes precedence. + items: + description: Rule defines the single authorization rule. + properties: + action: + description: Action defines the action to be taken if the + rule matches. + enum: + - Allow + - Deny + - Log + type: string + clientSelector: + description: ClientSelector contains the client selector + configuration. All selectors are AND'd together and only + if all selector are valid the Action is performed. + properties: + clientCIDR: + description: ClientCIDRs is a list of CIDRs. Valid examples + are "192.168.1.0/24" or "2001:db8::/64" + items: + type: string + type: array + type: object + required: + - action + type: object + minItems: 1 + type: array + type: object basicAuth: description: BasicAuth defines the configuration for the HTTP Basic Authentication. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index d485100eab3c..45dd4fa0713d 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -171,6 +171,20 @@ _Appears in:_ | `TCP` | ActiveHealthCheckerTypeTCP defines the TCP type of health checking.
| +#### Authorization + + + +Authorization defines the authorization configuration. + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `rules` | _[Rule](#rule) array_ | true | Rules contains all the authorization rules. | + + #### BackOffPolicy @@ -2865,6 +2879,38 @@ _Appears in:_ | `httpStatusCodes` | _[HTTPStatus](#httpstatus) array_ | false | HttpStatusCodes specifies the http status codes to be retried.
The retriable-status-codes trigger must also be configured for these status codes to trigger a retry. | +#### Rule + + + +Rule defines the single authorization rule. + +_Appears in:_ +- [Authorization](#authorization) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `subjects` | _[Subject](#subject) array_ | false | Subjects contains the subject configuration.
If empty, all subjects are included. | +| `permissions` | _string array_ | false | Permissions contains allowed HTTP methods.
If empty, all methods are matching. | +| `action` | _[RuleActionType](#ruleactiontype)_ | true | Action defines the action to be taken if the rule matches. | + + +#### RuleActionType + +_Underlying type:_ _string_ + +RuleActionType specifies the types of authorization rule action. + +_Appears in:_ +- [Rule](#rule) + +| Value | Description | +| ----- | ----------- | +| `Allow` | Allow is the action to allow the request.
| +| `Deny` | Deny is the action to deny the request.
| +| `Log` | Log is the action to log the request.
| + + #### SecurityPolicy @@ -2916,6 +2962,7 @@ _Appears in:_ | `jwt` | _[JWT](#jwt)_ | false | JWT defines the configuration for JSON Web Token (JWT) authentication. | | `oidc` | _[OIDC](#oidc)_ | false | OIDC defines the configuration for the OpenID Connect (OIDC) authentication. | | `extAuth` | _[ExtAuth](#extauth)_ | false | ExtAuth defines the configuration for External Authorization. | +| `authorization` | _[Authorization](#authorization)_ | false | Authorization defines the authorization configuration. | #### ServiceExternalTrafficPolicy @@ -3032,6 +3079,20 @@ _Appears in:_ | `RegularExpression` | StringMatchRegularExpression :The input string must match the regular expression
specified in the match value.
The regex string must adhere to the syntax documented in
https://github.com/google/re2/wiki/Syntax.
| +#### Subject + + + +Subject contains the subject configuration. + +_Appears in:_ +- [Rule](#rule) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `clientCIDR` | _string_ | false | ClientCIDR contains client cidr configuration.
Valid examples are "192.168.1.0/24" or "2001:db8::/64" | + + #### TCPActiveHealthChecker