Skip to content

Commit

Permalink
ExtAuth handler (#1258)
Browse files Browse the repository at this point in the history
* AuthorizationPolicy processor for ExtAuth

* consolidate pkg

* Revert "consolidate pkg"

This reverts commit 4ffbcff.

* ra

* Final touches before integration tests

* Add int tests

* Fix withFrom

* Add integration tests

* Update oauth2-server-mock.yaml

* Fix regex

* Remove requirement for more than one rule

* Add error log

* Revert old features

* Add validation

* Revert revert tests

* Adapt check to test ALLOW creation

* Change path

* Fix bad regex

* Fix init for custom label

* Docs

* Add restricions doc

* Add EOF

* review suggestions

* Apply suggestions from code review

Co-authored-by: Natalia Sitko <[email protected]>

* Apply documentation review remarks

* Split code for external authorizer validation

* Export CorsPolicyBuilder, because it is used by exported function.

* Introduce hook to update Istio CR ext auth config as part of the test run

* Introduce hook to update Istio CR ext auth config as part of the test run

* Introduce hook to update Istio CR ext auth config as part of the test run

* Store simplified ext-auth in v1beta1 and revert CRD validation

* Fix lint error

* Store all as annotation

* Remove parentheses

* Fix lint issue

* Add yet another unit test

* Update docs/release-notes/2.6.0.md

Co-authored-by: Natalia Sitko <[email protected]>

---------

Co-authored-by: Marek Kołodziejczak <[email protected]>
Co-authored-by: Marek Kolodziejczak <[email protected]>
Co-authored-by: Tim Riffer <[email protected]>
Co-authored-by: Natalia Sitko <[email protected]>
Co-authored-by: Tim Riffer <[email protected]>
  • Loading branch information
6 people authored Sep 10, 2024
1 parent ee87585 commit 19d1639
Show file tree
Hide file tree
Showing 86 changed files with 1,840 additions and 691 deletions.
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,11 @@ test: manifests generate fmt vet envtest ## Generate manifests and run tests.
test-integration: test-integration-v2alpha1 test-integration-ory test-integration-istio test-integration-gateway

.PHONY: test-integration-v2alpha1
test-integration-v2alpha1: generate fmt vet
test-integration-v2alpha1: generate fmt vet ## Run API Gateway integration tests with v2alpha1 API.
kubectl create ns ext-auth --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.23/samples/extauthz/ext-authz.yaml -n ext-auth
source ./tests/integration/env_vars.sh && go test -timeout 1h ./tests/integration -v -race -run TestV2alpha1
kubectl delete ns ext-auth

.PHONY: test-integration-ory
test-integration-ory: generate fmt vet
Expand Down
1 change: 0 additions & 1 deletion apis/gateway/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 44 additions & 15 deletions apis/gateway/v2alpha1/apirule_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package v2alpha1

import (
"encoding/json"
"k8s.io/apimachinery/pkg/runtime"
"time"

"github.com/kyma-project/api-gateway/apis/gateway/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/conversion"
)
Expand All @@ -32,6 +32,8 @@ func convertMap(m map[v1beta1.StatusCode]State) map[State]v1beta1.StatusCode {
// The 2 => 1 map is generated automatically based on 1 => 2 map
var alpha1to1beta1statusConversionMap = convertMap(beta1toV2alpha1StatusConversionMap)

const v2alpha1RulesAnnotationKey = "gateway.kyma-project.io/v2alpha1-rules"

// ConvertTo Converts this ApiRule (v2alpha1) to the Hub version (v1beta1)
func (apiRuleV2Alpha1 *APIRule) ConvertTo(hub conversion.Hub) error {
apiRuleBeta1 := hub.(*v1beta1.APIRule)
Expand Down Expand Up @@ -89,36 +91,55 @@ func (apiRuleV2Alpha1 *APIRule) ConvertTo(hub conversion.Hub) error {
}

if len(apiRuleV2Alpha1.Spec.Rules) > 0 {
marshaledApiRules, err := json.Marshal(apiRuleV2Alpha1.Spec.Rules)
if err != nil {
return err
}
if len(apiRuleBeta1.Annotations) == 0 {
apiRuleBeta1.Annotations = make(map[string]string)
}
apiRuleBeta1.Annotations[v2alpha1RulesAnnotationKey] = string(marshaledApiRules)

apiRuleBeta1.Spec.Rules = []v1beta1.Rule{}
for _, ruleV1Alpha2 := range apiRuleV2Alpha1.Spec.Rules {
for _, ruleV2Alpha1 := range apiRuleV2Alpha1.Spec.Rules {
ruleBeta1 := v1beta1.Rule{}
err = convertOverJson(ruleV1Alpha2, &ruleBeta1)
err = convertOverJson(ruleV2Alpha1, &ruleBeta1)
if err != nil {
return err
}
// No Auth
if ruleV1Alpha2.NoAuth != nil && *ruleV1Alpha2.NoAuth {

// ExtAuth
if ruleV2Alpha1.ExtAuth != nil {
ruleBeta1.AccessStrategies = append(ruleBeta1.AccessStrategies, &v1beta1.Authenticator{
Handler: &v1beta1.Handler{
Name: "ext-auth",
},
})
}

// NoAuth
if ruleV2Alpha1.NoAuth != nil && *ruleV2Alpha1.NoAuth {
ruleBeta1.AccessStrategies = append(ruleBeta1.AccessStrategies, &v1beta1.Authenticator{
Handler: &v1beta1.Handler{
Name: v1beta1.AccessStrategyNoAuth,
},
})
}
// JWT
if ruleV1Alpha2.Jwt != nil {
if ruleV2Alpha1.Jwt != nil {
ruleBeta1.AccessStrategies = append(ruleBeta1.AccessStrategies, &v1beta1.Authenticator{
Handler: &v1beta1.Handler{
Name: v1beta1.AccessStrategyJwt,
Config: &runtime.RawExtension{Object: ruleV1Alpha2.Jwt},
Config: &runtime.RawExtension{Object: ruleV2Alpha1.Jwt},
},
})
}

// Mutators
if ruleV1Alpha2.Request != nil {
if ruleV1Alpha2.Request.Cookies != nil {
if ruleV2Alpha1.Request != nil {
if ruleV2Alpha1.Request.Cookies != nil {
var config runtime.RawExtension
err := convertOverJson(ruleV1Alpha2.Request.Cookies, &config)
err := convertOverJson(ruleV2Alpha1.Request.Cookies, &config)
if err != nil {
return err
}
Expand All @@ -130,9 +151,9 @@ func (apiRuleV2Alpha1 *APIRule) ConvertTo(hub conversion.Hub) error {
})
}

if ruleV1Alpha2.Request.Headers != nil {
if ruleV2Alpha1.Request.Headers != nil {
var config runtime.RawExtension
err := convertOverJson(ruleV1Alpha2.Request.Headers, &config)
err := convertOverJson(ruleV2Alpha1.Request.Headers, &config)
if err != nil {
return err
}
Expand All @@ -148,7 +169,6 @@ func (apiRuleV2Alpha1 *APIRule) ConvertTo(hub conversion.Hub) error {
apiRuleBeta1.Spec.Rules = append(apiRuleBeta1.Spec.Rules, ruleBeta1)
}
}

return nil
}

Expand Down Expand Up @@ -215,7 +235,15 @@ func (apiRuleV2Alpha1 *APIRule) ConvertFrom(hub conversion.Hub) error {
*apiRuleV2Alpha1.Spec.Hosts[0] = Host(*apiRuleBeta1.Spec.Host)
}

if len(apiRuleBeta1.Spec.Rules) > 0 {
if annotation, ok := apiRuleBeta1.Annotations[v2alpha1RulesAnnotationKey]; ok {
var v2alpha1Rules []Rule
err := json.Unmarshal([]byte(annotation), &v2alpha1Rules)
if err != nil {
return err
}

apiRuleV2Alpha1.Spec.Rules = v2alpha1Rules
} else if len(apiRuleBeta1.Spec.Rules) > 0 {
apiRuleV2Alpha1.Spec.Rules = []Rule{}
for _, ruleBeta1 := range apiRuleBeta1.Spec.Rules {
ruleV1Alpha2 := Rule{}
Expand Down Expand Up @@ -268,6 +296,7 @@ func (apiRuleV2Alpha1 *APIRule) ConvertFrom(hub conversion.Hub) error {
}
apiRuleV2Alpha1.Spec.Rules = append(apiRuleV2Alpha1.Spec.Rules, ruleV1Alpha2)
}

}

return nil
Expand All @@ -292,7 +321,7 @@ func isFullConversionPossible(apiRule *v1beta1.APIRule) (bool, error) {
for _, rule := range apiRule.Spec.Rules {
for _, accessStrategy := range rule.AccessStrategies {

if accessStrategy.Handler.Name == v1beta1.AccessStrategyNoAuth {
if accessStrategy.Handler.Name == v1beta1.AccessStrategyNoAuth || accessStrategy.Handler.Name == "ext-auth" {
continue
}

Expand Down
15 changes: 14 additions & 1 deletion apis/gateway/v2alpha1/apirule_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ type Service struct {
}

// Rule .
// +kubebuilder:validation:XValidation:rule="has(self.jwt) ? !has(self.noAuth) || self.noAuth == false : has(self.noAuth) && self.noAuth == true",message="either jwt is configured or noAuth must be set to true in a rule"
// +kubebuilder:validation:XValidation:rule="((has(self.extAuth)?1:0)+(has(self.jwt)?1:0)+((has(self.noAuth)&&self.noAuth==true)?1:0))==1",message="One of the following fields must be set: noAuth, jwt, extAuth"
type Rule struct {
// Specifies the path of the exposed service.
// +kubebuilder:validation:Pattern=^([0-9a-zA-Z./*()?!\\_-]+)
Expand All @@ -134,6 +134,9 @@ type Rule struct {
// Specifies the Istio JWT access strategy.
// +optional
Jwt *JwtConfig `json:"jwt,omitempty"`
// Specifies external authorization configuration.
// +optional
ExtAuth *ExtAuth `json:"extAuth,omitempty"`
// +optional
Timeout *Timeout `json:"timeout,omitempty"`
// Request allows modifying the request before it is forwarded to the service.
Expand Down Expand Up @@ -194,6 +197,16 @@ type JwtHeader struct {
Prefix string `json:"prefix,omitempty"`
}

// ExtAuth contains configuration for paths that use external authorization.
type ExtAuth struct {
// Specifies the name of the external authorization handler.
// +optional
ExternalAuthorizers []string `json:"authorizers"`
// Specifies JWT configuration for the external authorization handler.
// +optional
Restrictions *JwtConfig `json:"restrictions,omitempty"`
}

// Timeout for HTTP requests in seconds. The timeout can be configured up to 3900 seconds (65 minutes).
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=3900
Expand Down
124 changes: 124 additions & 0 deletions apis/gateway/v2alpha1/extauth_conversion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package v2alpha1_test

import (
apirulev1beta1 "github.com/kyma-project/api-gateway/apis/gateway/v1beta1"
apirulev2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1"
v2alpha1 "github.com/kyma-project/api-gateway/internal/builders/builders_test/v2alpha1_test"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

const annotationKey = "gateway.kyma-project.io/v2alpha1-rules"

var dummyExtAuthRule = v2alpha1.NewRuleBuilder().
WithPath("/test").
WithMethods("GET").
WithExtAuth(v2alpha1.NewExtAuthBuilder().
WithAuthorizers("test-authorizer").
WithRestriction(&apirulev2alpha1.JwtConfig{
Authentications: []*apirulev2alpha1.JwtAuthentication{
{
Issuer: "test-issuer",
JwksUri: "test-jwks-uri",
FromHeaders: []*apirulev2alpha1.JwtHeader{
{
Name: "test-header",
Prefix: "test-prefix",
},
},
FromParams: []string{"test-param"},
},
},
Authorizations: nil,
}).
Build()).
Build()

var _ = Describe("ExtAuthStorage", func() {
It("Should store extAuth in v1beta1 through annotation and a rule with only handler name set", func() {
// given
v2alpha1APIRule := v2alpha1.NewAPIRuleBuilderWithDummyData().WithRules(dummyExtAuthRule).Build()

// when
var betaConverted apirulev1beta1.APIRule
err := v2alpha1APIRule.ConvertTo(&betaConverted)
Expect(err).ToNot(HaveOccurred())

//then
annotations := betaConverted.GetAnnotations()
Expect(annotations).To(HaveKey(annotationKey))
Expect(betaConverted.Spec.Rules).To(HaveLen(1))
Expect(betaConverted.Spec.Rules[0].Path).To(Equal("/test"))
Expect(betaConverted.Spec.Rules[0].Methods).To(BeEquivalentTo([]apirulev1beta1.HttpMethod{"GET"}))
Expect(betaConverted.Spec.Rules[0].AccessStrategies).To(HaveLen(1))
Expect(betaConverted.Spec.Rules[0].AccessStrategies[0].Handler.Name).To(BeEquivalentTo("ext-auth"))
Expect(betaConverted.Spec.Rules[0].AccessStrategies[0].Config).To(BeNil())
})
})

var _ = Describe("ExtAuthConversion", func() {

DescribeTable("Should convert back and forth correctly with ExtAuth set", func(expectedRules []*apirulev2alpha1.Rule) {
// given
v2alpha1APIRule := v2alpha1.NewAPIRuleBuilderWithDummyData().WithRules(expectedRules...).Build()
var betaConverted apirulev1beta1.APIRule
err := v2alpha1APIRule.ConvertTo(&betaConverted)
Expect(err).ToNot(HaveOccurred())

// when
var v2alpha1ConvertedRule apirulev2alpha1.APIRule
err = v2alpha1ConvertedRule.ConvertFrom(&betaConverted)

// then
Expect(err).ToNot(HaveOccurred())
Expect(v2alpha1ConvertedRule.Spec.Rules).To(HaveLen(len(expectedRules)))
for i, rule := range v2alpha1ConvertedRule.Spec.Rules {
Expect(rule.Path).To(Equal(expectedRules[i].Path))
Expect(rule.Methods).To(BeEquivalentTo(expectedRules[i].Methods))
Expect(rule.Service).To(BeEquivalentTo(expectedRules[i].Service))
Expect(rule.NoAuth).To(Equal(expectedRules[i].NoAuth))
Expect(rule.Jwt != nil).To(Equal(expectedRules[i].Jwt != nil))
if rule.Jwt != nil {
Expect(rule.Jwt.Authorizations).To(BeEquivalentTo(expectedRules[i].Jwt.Authorizations))
Expect(rule.Jwt.Authentications).To(BeEquivalentTo(expectedRules[i].Jwt.Authentications))
}
Expect(rule.ExtAuth != nil).To(Equal(expectedRules[i].ExtAuth != nil))
if rule.ExtAuth != nil {
Expect(rule.ExtAuth.ExternalAuthorizers).To(BeEquivalentTo(expectedRules[i].ExtAuth.ExternalAuthorizers))
Expect(rule.ExtAuth.Restrictions != nil).To(Equal(expectedRules[i].ExtAuth.Restrictions != nil))
if rule.ExtAuth.Restrictions != nil {
Expect(rule.ExtAuth.Restrictions.Authentications).To(BeEquivalentTo(expectedRules[i].ExtAuth.Restrictions.Authentications))
Expect(rule.ExtAuth.Restrictions.Authorizations).To(BeEquivalentTo(expectedRules[i].ExtAuth.Restrictions.Authorizations))
}
}
}
},
Entry("Should convert APIRule with no ExtAuth", []*apirulev2alpha1.Rule{}),
Entry("Should convert APIRule with only ExtAuth", []*apirulev2alpha1.Rule{dummyExtAuthRule}),
Entry("Should preserve order of rules when ExtAuth is in the middle", []*apirulev2alpha1.Rule{
v2alpha1.NewRuleBuilder().
WithPath("/first").
NoAuth().
Build(),
dummyExtAuthRule,
v2alpha1.NewRuleBuilder().
WithPath("/third").
NoAuth().
Build(),
}),
Entry("Should preserve order of rules when ExtAuth is at the end", []*apirulev2alpha1.Rule{
v2alpha1.NewRuleBuilder().
WithPath("/first").
NoAuth().
Build(),
dummyExtAuthRule,
}),
Entry("Should preserve order of rules when ExtAuth is at the beginning", []*apirulev2alpha1.Rule{
dummyExtAuthRule,
v2alpha1.NewRuleBuilder().
WithPath("/second").
NoAuth().
Build(),
}),
)
})
19 changes: 12 additions & 7 deletions apis/gateway/v2alpha1/jwt_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package v2alpha1

import (
"encoding/json"

"errors"
"github.com/kyma-project/api-gateway/apis/gateway/v1beta1"
"github.com/kyma-project/api-gateway/internal/types/ory"
)
Expand Down Expand Up @@ -35,19 +35,24 @@ func convertOryJwtAccessStrategy(accessStrategy *v1beta1.Authenticator) (*v1beta
}

func convertIstioJwtAccessStrategy(accessStrategy *v1beta1.Authenticator) (*v1beta1.JwtConfig, error) {
var jwtConfig *v1beta1.JwtConfig

if accessStrategy.Config.Object != nil {
jwtConfig = accessStrategy.Config.Object.(*v1beta1.JwtConfig)
} else if accessStrategy.Config.Raw != nil {
jwtConfig = &v1beta1.JwtConfig{}
err := json.Unmarshal(accessStrategy.Config.Raw, jwtConfig)
jwtConfig, ok := accessStrategy.Config.Object.(*v1beta1.JwtConfig)
if ok {
return jwtConfig, nil
}
}

if accessStrategy.Config.Raw != nil {
var jwtConfig v1beta1.JwtConfig
err := json.Unmarshal(accessStrategy.Config.Raw, &jwtConfig)
if err != nil {
return nil, err
}
return &jwtConfig, nil
}

return jwtConfig, nil
return nil, errors.New("no raw config to convert")
}

func isConvertibleJwtConfig(accessStrategy *v1beta1.Authenticator) (bool, error) {
Expand Down
Loading

0 comments on commit 19d1639

Please sign in to comment.