diff --git a/controllers/apirule_controller.go b/controllers/apirule_controller.go index b2fe88308..900bb852f 100644 --- a/controllers/apirule_controller.go +++ b/controllers/apirule_controller.go @@ -22,14 +22,14 @@ import ( "fmt" "time" - gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" - "github.com/kyma-incubator/api-gateway/internal/helpers" - "github.com/kyma-incubator/api-gateway/internal/processing" + "github.com/kyma-incubator/api-gateway/internal/processing/istio" + "github.com/kyma-incubator/api-gateway/internal/processing/ory" "github.com/kyma-incubator/api-gateway/internal/validation" "github.com/go-logr/logr" - networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/processing" corev1 "k8s.io/api/core/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -134,76 +134,61 @@ func (r *APIRuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct failuresJson, _ := json.Marshal(configValidationFailures) r.Log.Error(err, fmt.Sprintf(`Config validation failure {"controller": "Api", "failures": %s}`, string(failuresJson))) } - return r.doneReconcile() + return doneReconcile() } } - api := &gatewayv1beta1.APIRule{} - err := r.Client.Get(ctx, req.NamespacedName, api) + apiRule := &gatewayv1beta1.APIRule{} + err := r.Client.Get(ctx, req.NamespacedName, apiRule) if err != nil { if apierrs.IsNotFound(err) { //There is no APIRule. Nothing to process, dependent objects will be garbage-collected. - return r.doneReconcile() + return doneReconcile() } //Nothing is yet processed: StatusSkipped - return r.setStatusForError(ctx, api, err, gatewayv1beta1.StatusSkipped) + status := processing.GetStatusForError(&r.Log, err, gatewayv1beta1.StatusSkipped) + return r.updateStatusOrRetry(ctx, apiRule, status) } configValidationFailures := validator.ValidateConfig(r.Config) if len(configValidationFailures) > 0 { failuresJson, _ := json.Marshal(configValidationFailures) - r.Log.Error(err, fmt.Sprintf(`Config validation failure {"controller": "Api", "request": "%s/%s", "failures": %s}`, api.Namespace, api.Name, string(failuresJson))) - return r.setStatus(ctx, api, generateValidationStatus(configValidationFailures), gatewayv1beta1.StatusError) + r.Log.Error(err, fmt.Sprintf(`Config validation failure {"controller": "Api", "request": "%s/%s", "failures": %s}`, apiRule.Namespace, apiRule.Name, string(failuresJson))) + return r.updateStatusOrRetry(ctx, apiRule, processing.GetFailedValidationStatus(configValidationFailures)) } //Prevent reconciliation after status update. It should be solved by controller-runtime implementation but still isn't. - if api.Generation != api.Status.ObservedGeneration { - //1.1) Get the list of existing Virtual Services to validate host - var vsList networkingv1beta1.VirtualServiceList - if err := r.Client.List(ctx, &vsList); err != nil { - //Nothing is yet processed: StatusSkipped - return r.setStatusForError(ctx, api, err, gatewayv1beta1.StatusSkipped) + if apiRule.Generation != apiRule.Status.ObservedGeneration { + + c := processing.ReconciliationConfig{ + OathkeeperSvc: r.OathkeeperSvc, + OathkeeperSvcPort: r.OathkeeperSvcPort, + CorsConfig: r.CorsConfig, + AdditionalLabels: r.GeneratedObjectsLabels, + DefaultDomainName: r.DefaultDomainName, + ServiceBlockList: r.ServiceBlockList, + DomainAllowList: r.DomainAllowList, + HostBlockList: r.HostBlockList, } - //1.2) Validate input including host - validationFailures := validator.Validate(api, vsList, r.Config) - if len(validationFailures) > 0 { - failuresJson, _ := json.Marshal(validationFailures) - r.Log.Info(fmt.Sprintf(`Validation failure {"controller": "Api", "request": "%s/%s", "failures": %s}`, api.Namespace, api.Name, string(failuresJson))) - return r.setStatus(ctx, api, generateValidationStatus(validationFailures), gatewayv1beta1.StatusSkipped) - } + cmd := r.getReconciliation(c) - //2) Compute list of required objects (the set of objects required to satisfy our contract on apiRule.Spec, not yet applied) - factory := processing.NewFactory(r.Client, r.Log, r.OathkeeperSvc, r.OathkeeperSvcPort, r.CorsConfig, r.GeneratedObjectsLabels, r.DefaultDomainName) - requiredObjects := factory.CalculateRequiredState(api, r.Config) + status := processing.Reconcile(ctx, r.Client, &r.Log, cmd, apiRule) - //3.1 Fetch all existing objects related to _this_ apiRule from the cluster (VS, Rules) - actualObjects, err := factory.GetActualState(ctx, api, r.Config) - if err != nil { - return r.setStatusForError(ctx, api, err, gatewayv1beta1.StatusSkipped) - } - - //3.2 Compute patch object - patch := factory.CalculateDiff(requiredObjects, actualObjects, r.Config) - - //3.3 Apply changes to the cluster - err = factory.ApplyDiff(ctx, patch, r.Config) - if err != nil { - //We don't know exactly which object(s) are not updated properly. - //The safest approach is to assume nothing is correct and just use `StatusError`. - return r.setStatusForError(ctx, api, err, gatewayv1beta1.StatusError) - } + return r.updateStatusOrRetry(ctx, apiRule, status) + } - //4) Update status of CR - APIStatus := &gatewayv1beta1.APIRuleResourceStatus{ - Code: gatewayv1beta1.StatusOK, - } + return doneReconcile() +} - return r.setStatus(ctx, api, APIStatus, gatewayv1beta1.StatusOK) +func (r *APIRuleReconciler) getReconciliation(config processing.ReconciliationConfig) processing.ReconciliationCommand { + if r.Config.JWTHandler == helpers.JWT_HANDLER_ISTIO { + return istio.NewIstioReconciliation(config) } - return r.doneReconcile() + return ory.NewOryReconciliation(config) + } // SetupWithManager sets up the controller with the Manager. @@ -215,56 +200,17 @@ func (r *APIRuleReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -// Sets status of APIRule. Accepts an auxilary status code that is used to report VirtualService, AccessRule, RequestAuthentication and AuthorizationPolicy statuses. -func (r *APIRuleReconciler) setStatus(ctx context.Context, api *gatewayv1beta1.APIRule, apiStatus *gatewayv1beta1.APIRuleResourceStatus, auxStatusCode gatewayv1beta1.StatusCode) (ctrl.Result, error) { - virtualServiceStatus := &gatewayv1beta1.APIRuleResourceStatus{ - Code: auxStatusCode, - } - accessRuleStatus := &gatewayv1beta1.APIRuleResourceStatus{ - Code: auxStatusCode, - } - reqAuthStatus := &gatewayv1beta1.APIRuleResourceStatus{ - Code: auxStatusCode, - } - authPolicyStatus := &gatewayv1beta1.APIRuleResourceStatus{ - Code: auxStatusCode, - } - - return r.updateStatusOrRetry(ctx, api, apiStatus, virtualServiceStatus, accessRuleStatus, reqAuthStatus, authPolicyStatus) -} - -// Sets status of APIRule in error condition. Accepts an auxilary status code that is used to report VirtualService, AccessRule, RequestAuthentication and AuthorizationPolicy statuses. -func (r *APIRuleReconciler) setStatusForError(ctx context.Context, api *gatewayv1beta1.APIRule, err error, auxStatusCode gatewayv1beta1.StatusCode) (ctrl.Result, error) { - r.Log.Error(err, "Error during reconciliation") - - virtualServiceStatus := &gatewayv1beta1.APIRuleResourceStatus{ - Code: auxStatusCode, - } - accessRuleStatus := &gatewayv1beta1.APIRuleResourceStatus{ - Code: auxStatusCode, - } - reqAuthStatus := &gatewayv1beta1.APIRuleResourceStatus{ - Code: auxStatusCode, - } - authPolicyStatus := &gatewayv1beta1.APIRuleResourceStatus{ - Code: auxStatusCode, - } - - return r.updateStatusOrRetry(ctx, api, generateErrorStatus(err), virtualServiceStatus, accessRuleStatus, reqAuthStatus, authPolicyStatus) -} - // Updates api status. If there was an error during update, returns the error so that entire reconcile loop is retried. If there is no error, returns a "reconcile success" value. -func (r *APIRuleReconciler) updateStatusOrRetry(ctx context.Context, api *gatewayv1beta1.APIRule, apiStatus, virtualServiceStatus, accessRuleStatus, reqAuthStatus, authPolicyStatus *gatewayv1beta1.APIRuleResourceStatus) (ctrl.Result, error) { - _, updateStatusErr := r.updateStatus(ctx, api, apiStatus, virtualServiceStatus, accessRuleStatus, reqAuthStatus, authPolicyStatus) +func (r *APIRuleReconciler) updateStatusOrRetry(ctx context.Context, api *gatewayv1beta1.APIRule, status processing.ReconciliationStatus) (ctrl.Result, error) { + _, updateStatusErr := r.updateStatus(ctx, api, status) if updateStatusErr != nil { return retryReconcile(updateStatusErr) //controller retries to set the correct status eventually. } //Fail fast: If status is updated, users are informed about the problem. We don't need to reconcile again. - return r.doneReconcile() + return doneReconcile() } -func (r *APIRuleReconciler) doneReconcile() (ctrl.Result, error) { - r.Log.Info("Ending reconcilation") +func doneReconcile() (ctrl.Result, error) { return ctrl.Result{}, nil } @@ -272,14 +218,14 @@ func retryReconcile(err error) (ctrl.Result, error) { return reconcile.Result{Requeue: true}, err } -func (r *APIRuleReconciler) updateStatus(ctx context.Context, api *gatewayv1beta1.APIRule, APIStatus, virtualServiceStatus, accessRuleStatus, reqAuthStatus, authPolicyStatus *gatewayv1beta1.APIRuleResourceStatus) (*gatewayv1beta1.APIRule, error) { +func (r *APIRuleReconciler) updateStatus(ctx context.Context, api *gatewayv1beta1.APIRule, status processing.ReconciliationStatus) (*gatewayv1beta1.APIRule, error) { api.Status.ObservedGeneration = api.Generation api.Status.LastProcessedTime = &v1.Time{Time: time.Now()} - api.Status.APIRuleStatus = APIStatus - api.Status.VirtualServiceStatus = virtualServiceStatus - api.Status.AccessRuleStatus = accessRuleStatus - api.Status.RequestAuthenticationStatus = reqAuthStatus - api.Status.AuthorizationPolicyStatus = authPolicyStatus + api.Status.APIRuleStatus = status.ApiRuleStatus + api.Status.VirtualServiceStatus = status.VirtualServiceStatus + api.Status.AccessRuleStatus = status.AccessRuleStatus + api.Status.RequestAuthenticationStatus = status.RequestAuthenticationStatus + api.Status.AuthorizationPolicyStatus = status.AuthorizationPolicyStatus err := r.Client.Status().Update(ctx, api) if err != nil { @@ -287,38 +233,3 @@ func (r *APIRuleReconciler) updateStatus(ctx context.Context, api *gatewayv1beta } return api, nil } - -func generateErrorStatus(err error) *gatewayv1beta1.APIRuleResourceStatus { - return toStatus(gatewayv1beta1.StatusError, err.Error()) -} - -func generateValidationStatus(failures []validation.Failure) *gatewayv1beta1.APIRuleResourceStatus { - return toStatus(gatewayv1beta1.StatusError, generateValidationDescription(failures)) -} - -func toStatus(c gatewayv1beta1.StatusCode, desc string) *gatewayv1beta1.APIRuleResourceStatus { - return &gatewayv1beta1.APIRuleResourceStatus{ - Code: c, - Description: desc, - } -} - -func generateValidationDescription(failures []validation.Failure) string { - var description string - - if len(failures) == 1 { - description = "Validation error: " - description += fmt.Sprintf("Attribute \"%s\": %s", failures[0].AttributePath, failures[0].Message) - } else { - const maxEntries = 3 - description = "Multiple validation errors: " - for i := 0; i < len(failures) && i < maxEntries; i++ { - description += fmt.Sprintf("\nAttribute \"%s\": %s", failures[i].AttributePath, failures[i].Message) - } - if len(failures) > maxEntries { - description += fmt.Sprintf("\n%d more error(s)...", len(failures)-maxEntries) - } - } - - return description -} diff --git a/internal/processing/access_rule_processor.go b/internal/processing/access_rule_processor.go new file mode 100644 index 000000000..d9b996bc6 --- /dev/null +++ b/internal/processing/access_rule_processor.go @@ -0,0 +1,105 @@ +package processing + +import ( + "context" + "fmt" + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + rulev1alpha1 "github.com/ory/oathkeeper-maester/api/v1alpha1" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +// AccessRuleProcessor is the generic processor that handles the Ory Rules in the reconciliation of API Rule. +type AccessRuleProcessor struct { + Creator AccessRuleCreator +} + +// AccessRuleCreator provides the creation of Rules using the configuration in the given APIRule. +// The key of the map is expected to be unique and comparable with the +type AccessRuleCreator interface { + Create(api *gatewayv1beta1.APIRule) map[string]*rulev1alpha1.Rule +} + +func (r AccessRuleProcessor) EvaluateReconciliation(ctx context.Context, client ctrlclient.Client, apiRule *gatewayv1beta1.APIRule) ([]*ObjectChange, error) { + desired := r.getDesiredState(apiRule) + actual, err := r.getActualState(ctx, client, apiRule) + if err != nil { + return make([]*ObjectChange, 0), err + } + + c := r.getObjectChanges(desired, actual) + + return c, nil +} + +func (r AccessRuleProcessor) getObjectChanges(desiredRules map[string]*rulev1alpha1.Rule, actualRules map[string]*rulev1alpha1.Rule) []*ObjectChange { + arChanges := make(map[string]*ObjectChange) + + for path, rule := range desiredRules { + + if actualRules[path] != nil { + actualRules[path].Spec = rule.Spec + arChanges[path] = NewObjectUpdateAction(actualRules[path]) + } else { + arChanges[path] = NewObjectCreateAction(rule) + } + + } + + for path, rule := range actualRules { + if desiredRules[path] == nil { + arChanges[path] = NewObjectDeleteAction(rule) + } + } + + arChangesToApply := make([]*ObjectChange, 0, len(arChanges)) + + for _, applyCommand := range arChanges { + arChangesToApply = append(arChangesToApply, applyCommand) + } + + return arChangesToApply +} + +func (r AccessRuleProcessor) getDesiredState(api *gatewayv1beta1.APIRule) map[string]*rulev1alpha1.Rule { + return r.Creator.Create(api) +} + +func (r AccessRuleProcessor) getActualState(ctx context.Context, client ctrlclient.Client, api *gatewayv1beta1.APIRule) (map[string]*rulev1alpha1.Rule, error) { + labels := GetOwnerLabels(api) + + var arList rulev1alpha1.RuleList + if err := client.List(ctx, &arList, ctrlclient.MatchingLabels(labels)); err != nil { + return nil, err + } + + accessRules := make(map[string]*rulev1alpha1.Rule) + pathDuplicates := HasPathDuplicates(api.Spec.Rules) + + for i := range arList.Items { + obj := arList.Items[i] + accessRules[SetAccessRuleKey(pathDuplicates, obj)] = &obj + } + + return accessRules, nil +} + +func SetAccessRuleKey(hasPathDuplicates bool, rule rulev1alpha1.Rule) string { + + if hasPathDuplicates { + return fmt.Sprintf("%s:%s", rule.Spec.Match.URL, rule.Spec.Match.Methods) + } + + return rule.Spec.Match.URL +} + +func HasPathDuplicates(rules []gatewayv1beta1.Rule) bool { + duplicates := map[string]bool{} + for _, rule := range rules { + if duplicates[rule.Path] { + return true + } + duplicates[rule.Path] = true + } + + return false +} diff --git a/internal/processing/access_rule_processor_test.go b/internal/processing/access_rule_processor_test.go new file mode 100644 index 000000000..1f2b37458 --- /dev/null +++ b/internal/processing/access_rule_processor_test.go @@ -0,0 +1,274 @@ +package processing_test + +import ( + "context" + "fmt" + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/builders" + "github.com/kyma-incubator/api-gateway/internal/processing" + . "github.com/kyma-incubator/api-gateway/internal/processing/internal/test" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + rulev1alpha1 "github.com/ory/oathkeeper-maester/api/v1alpha1" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var _ = Describe("Access Rule Processor", func() { + It("should create access rule when no exists", func() { + // given + apiRule := &gatewayv1beta1.APIRule{} + + processor := processing.AccessRuleProcessor{ + Creator: mockCreator{ + createMock: func() map[string]*rulev1alpha1.Rule { + return map[string]*rulev1alpha1.Rule{ + "://myService.myDomain.com": builders.AccessRule().Spec( + builders.AccessRuleSpec().Match( + builders.Match().URL("://myService.myDomain.com"))).Get(), + } + }, + }, + } + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), GetEmptyFakeClient(), apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Action.String()).To(Equal("create")) + }) + + It("should update access rule when path exists", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + noopRule := GetRuleFor("path", ApiMethods, []*gatewayv1beta1.Mutator{}, noop) + rules := []gatewayv1beta1.Rule{noopRule} + + apiRule := GetAPIRuleFor(rules) + + rule := rulev1alpha1.Rule{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + processing.OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), + }, + }, + Spec: rulev1alpha1.RuleSpec{ + Match: &rulev1alpha1.Match{ + URL: fmt.Sprintf("://%s<%s>", ServiceHost, "path"), + }, + }, + } + + vs := networkingv1beta1.VirtualService{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + processing.OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), + }, + }, + } + + scheme := runtime.NewScheme() + err := rulev1alpha1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + err = networkingv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + err = gatewayv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&rule, &vs).Build() + processor := processing.AccessRuleProcessor{ + Creator: mockCreator{ + createMock: func() map[string]*rulev1alpha1.Rule { + return map[string]*rulev1alpha1.Rule{ + "://myService.myDomain.com": builders.AccessRule().Spec( + builders.AccessRuleSpec().Match( + builders.Match().URL("://myService.myDomain.com"))).Get(), + } + }, + }, + } + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Action.String()).To(Equal("update")) + }) + + It("should delete access rule", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + noopRule := GetRuleFor("same", ApiMethods, []*gatewayv1beta1.Mutator{}, noop) + rules := []gatewayv1beta1.Rule{noopRule} + + apiRule := GetAPIRuleFor(rules) + + rule := rulev1alpha1.Rule{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + processing.OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), + }, + }, + Spec: rulev1alpha1.RuleSpec{ + Match: &rulev1alpha1.Match{ + URL: fmt.Sprintf("://%s<%s>", ServiceHost, "path"), + }, + }, + } + + vs := networkingv1beta1.VirtualService{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + processing.OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), + }, + }, + } + + scheme := runtime.NewScheme() + err := rulev1alpha1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + err = networkingv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + err = gatewayv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&rule, &vs).Build() + processor := processing.AccessRuleProcessor{ + Creator: mockCreator{ + createMock: func() map[string]*rulev1alpha1.Rule { + return map[string]*rulev1alpha1.Rule{} + }, + }, + } + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Action.String()).To(Equal("delete")) + }) + + When("rule exists and and rule path is different", func() { + It("should create new rule and delete old rule", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + noopRule := GetRuleFor("newPath", ApiMethods, []*gatewayv1beta1.Mutator{}, noop) + rules := []gatewayv1beta1.Rule{noopRule} + + apiRule := GetAPIRuleFor(rules) + + rule := rulev1alpha1.Rule{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + processing.OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), + }, + }, + Spec: rulev1alpha1.RuleSpec{ + Match: &rulev1alpha1.Match{ + URL: fmt.Sprintf("://%s<%s>", ServiceHost, "oldPath"), + }, + }, + } + + vs := networkingv1beta1.VirtualService{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + processing.OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), + }, + }, + } + + scheme := runtime.NewScheme() + err := rulev1alpha1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + err = networkingv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + err = gatewayv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&rule, &vs).Build() + processor := processing.AccessRuleProcessor{ + Creator: mockCreator{ + createMock: func() map[string]*rulev1alpha1.Rule { + return map[string]*rulev1alpha1.Rule{ + "://myService.myDomain.com": builders.AccessRule().Spec( + builders.AccessRuleSpec().Match( + builders.Match().URL("://myService.myDomain.com"))).Get(), + } + }, + }, + } + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(2)) + + createResultMatcher := PointTo(MatchFields(IgnoreExtras, Fields{ + "Action": WithTransform(actionToString, Equal("create")), + "Obj": PointTo(MatchFields(IgnoreExtras, Fields{ + "Spec": MatchFields(IgnoreExtras, Fields{ + "Match": PointTo(MatchFields(IgnoreExtras, Fields{ + "URL": Equal("://myService.myDomain.com"), + })), + }), + })), + })) + + deleteResultMatcher := PointTo(MatchFields(IgnoreExtras, Fields{ + "Action": WithTransform(actionToString, Equal("delete")), + "Obj": PointTo(MatchFields(IgnoreExtras, Fields{ + "Spec": MatchFields(IgnoreExtras, Fields{ + "Match": PointTo(MatchFields(IgnoreExtras, Fields{ + "URL": Equal("://myService.myDomain.com"), + })), + }), + })), + })) + + Expect(result).To(ContainElements(createResultMatcher, deleteResultMatcher)) + }) + }) +}) + +type mockCreator struct { + createMock func() map[string]*rulev1alpha1.Rule +} + +func (r mockCreator) Create(_ *gatewayv1beta1.APIRule) map[string]*rulev1alpha1.Rule { + return r.createMock() +} + +var actionToString = func(a processing.Action) string { return a.String() } diff --git a/internal/processing/authorization_policy_helpers.go b/internal/processing/authorization_policy_helpers.go deleted file mode 100644 index 464c8006a..000000000 --- a/internal/processing/authorization_policy_helpers.go +++ /dev/null @@ -1,52 +0,0 @@ -package processing - -import ( - "fmt" - "istio.io/api/security/v1beta1" - - gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" - "github.com/kyma-incubator/api-gateway/internal/builders" - securityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" -) - -func modifyAuthorizationPolicy(existing, required *securityv1beta1.AuthorizationPolicy) { - existing.Spec = *required.Spec.DeepCopy() -} - -func generateAuthorizationPolicy(api *gatewayv1beta1.APIRule, rule gatewayv1beta1.Rule, additionalLabels map[string]string) *securityv1beta1.AuthorizationPolicy { - namePrefix := fmt.Sprintf("%s-", api.ObjectMeta.Name) - namespace := api.ObjectMeta.Namespace - ownerRef := generateOwnerRef(api) - - arBuilder := builders.AuthorizationPolicyBuilder(). - GenerateName(namePrefix). - Namespace(namespace). - Owner(builders.OwnerReference().From(&ownerRef)). - Spec(builders.AuthorizationPolicySpecBuilder().From(generateAuthorizationPolicySpec(api, rule))). - Label(OwnerLabel, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)). - Label(OwnerLabelv1alpha1, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)) - - for k, v := range additionalLabels { - arBuilder.Label(k, v) - } - - return arBuilder.Get() -} - -func generateAuthorizationPolicySpec(api *gatewayv1beta1.APIRule, rule gatewayv1beta1.Rule) *v1beta1.AuthorizationPolicy { - var serviceName string - if rule.Service != nil { - serviceName = *rule.Service.Name - } else { - serviceName = *api.Spec.Service.Name - } - - authorizationPolicySpec := builders.AuthorizationPolicySpecBuilder(). - Selector(builders.SelectorBuilder().MatchLabels("app", serviceName)). - Rule(builders.RuleBuilder(). - RuleFrom(builders.RuleFromBuilder().Source()). - RuleTo(builders.RuleToBuilder(). - Operation(builders.OperationBuilder().Methods(rule.Methods).Path(rule.Path)))) - - return authorizationPolicySpec.Get() -} diff --git a/internal/processing/helpers.go b/internal/processing/helpers.go index 65a776c45..c7c2b308c 100644 --- a/internal/processing/helpers.go +++ b/internal/processing/helpers.go @@ -3,26 +3,20 @@ package processing import ( "fmt" + gatewayv1alpha1 "github.com/kyma-incubator/api-gateway/api/v1alpha1" gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" "github.com/kyma-incubator/api-gateway/internal/builders" - rulev1alpha1 "github.com/ory/oathkeeper-maester/api/v1alpha1" - istiosecurityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" k8sMeta "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func isSecured(rule gatewayv1beta1.Rule) bool { - if len(rule.Mutators) > 0 { - return true - } - for _, strat := range rule.AccessStrategies { - if strat.Name != "allow" { - return true - } - } - return false -} +var ( + //OwnerLabel . + OwnerLabel = fmt.Sprintf("%s.%s", "apirule", gatewayv1beta1.GroupVersion.String()) + //OwnerLabelv1alpha1 . + OwnerLabelv1alpha1 = fmt.Sprintf("%s.%s", "apirule", gatewayv1alpha1.GroupVersion.String()) +) -func isJwtSecured(rule gatewayv1beta1.Rule) bool { +func IsJwtSecured(rule gatewayv1beta1.Rule) bool { for _, strat := range rule.AccessStrategies { if strat.Name == "jwt" { return true @@ -31,63 +25,19 @@ func isJwtSecured(rule gatewayv1beta1.Rule) bool { return false } -func checkPathDuplicates(rules []gatewayv1beta1.Rule) bool { - duplicates := map[string]bool{} - for _, rule := range rules { - if duplicates[rule.Path] { +func IsSecured(rule gatewayv1beta1.Rule) bool { + if len(rule.Mutators) > 0 { + return true + } + for _, strat := range rule.AccessStrategies { + if strat.Name != "allow" { return true } - duplicates[rule.Path] = true } - return false } -func filterDuplicatePaths(rules []gatewayv1beta1.Rule) []gatewayv1beta1.Rule { - duplicates := make(map[string]bool) - var filteredRules []gatewayv1beta1.Rule - for _, rule := range rules { - if _, exists := duplicates[rule.Path]; !exists { - duplicates[rule.Path] = true - filteredRules = append(filteredRules, rule) - } - } - - return filteredRules -} - -func setAccessRuleKey(hasPathDuplicates bool, rule rulev1alpha1.Rule) string { - if hasPathDuplicates { - return fmt.Sprintf("%s:%s", rule.Spec.Match.URL, rule.Spec.Match.Methods) - } - - return rule.Spec.Match.URL -} - -func getAuthorizationPolicyKey(hasPathDuplicates bool, ap *istiosecurityv1beta1.AuthorizationPolicy) string { - key := "" - if ap.Spec.Rules != nil && len(ap.Spec.Rules) > 0 && ap.Spec.Rules[0].To != nil && len(ap.Spec.Rules[0].To) > 0 { - if hasPathDuplicates { - key = fmt.Sprintf("%s:%s", - sliceToString(ap.Spec.Rules[0].To[0].Operation.Paths), - sliceToString(ap.Spec.Rules[0].To[0].Operation.Methods)) - } else { - key = sliceToString(ap.Spec.Rules[0].To[0].Operation.Paths) - } - } - - return key -} - -func getRequestAuthenticationKey(ra *istiosecurityv1beta1.RequestAuthentication) string { - key := "" - for _, k := range ra.Spec.JwtRules { - key += fmt.Sprintf("%s:%s", k.Issuer, k.JwksUri) - } - return key -} - -func generateOwnerRef(api *gatewayv1beta1.APIRule) k8sMeta.OwnerReference { +func GenerateOwnerRef(api *gatewayv1beta1.APIRule) k8sMeta.OwnerReference { return *builders.OwnerReference(). Name(api.ObjectMeta.Name). APIVersion(api.TypeMeta.APIVersion). @@ -97,9 +47,8 @@ func generateOwnerRef(api *gatewayv1beta1.APIRule) k8sMeta.OwnerReference { Get() } -func sliceToString(ss []string) (s string) { - for _, el := range ss { - s += el - } - return +func GetOwnerLabels(api *gatewayv1beta1.APIRule) map[string]string { + labels := make(map[string]string) + labels[OwnerLabelv1alpha1] = fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace) + return labels } diff --git a/internal/processing/internal/test/test_utils.go b/internal/processing/internal/test/test_utils.go new file mode 100644 index 000000000..6d8da354a --- /dev/null +++ b/internal/processing/internal/test/test_utils.go @@ -0,0 +1,137 @@ +package processing_test + +import ( + v1beta12 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/processing" + "github.com/onsi/gomega" + rulev1alpha1 "github.com/ory/oathkeeper-maester/api/v1alpha1" + "istio.io/api/networking/v1beta1" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + securityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const ( + ApiName = "test-apirule" + ApiUID types.UID = "eab0f1c8-c417-11e9-bf11-4ac644044351" + ApiNamespace = "some-namespace" + ApiAPIVersion = "gateway.kyma-project.io/v1alpha1" + ApiKind = "ApiRule" + ApiPath = "/.*" + HeadersApiPath = "/headers" + ImgApiPath = "/img" + JwtIssuer = "https://oauth2.example.com/" + JwksUri = "https://oauth2.example.com/.well-known/jwks.json" + JwtIssuer2 = "https://oauth2.another.example.com/" + JwksUri2 = "https://oauth2.another.example.com/.well-known/jwks.json" + OathkeeperSvc = "fake.oathkeeper" + OathkeeperSvcPort uint32 = 1234 + TestLabelKey = "key" + TestLabelValue = "value" + DefaultDomain = "myDomain.com" + TestSelectorKey = "app" +) + +var ( + ApiMethods = []string{"GET"} + ApiScopes = []string{"write", "read"} + ServicePort uint32 = 8080 + ApiGateway = "some-gateway" + ServiceName = "example-service" + ServiceHostWithNoDomain = "myService" + ServiceHost = ServiceHostWithNoDomain + "." + DefaultDomain + + TestAllowOrigin = []*v1beta1.StringMatch{{MatchType: &v1beta1.StringMatch_Regex{Regex: ".*"}}} + TestAllowMethods = []string{"GET", "POST", "PUT", "DELETE"} + TestAllowHeaders = []string{"header1", "header2"} + + TestCors = &processing.CorsConfig{ + AllowOrigins: TestAllowOrigin, + AllowMethods: TestAllowMethods, + AllowHeaders: TestAllowHeaders, + } + + TestAdditionalLabels = map[string]string{TestLabelKey: TestLabelValue} +) + +func GetTestConfig() processing.ReconciliationConfig { + return processing.ReconciliationConfig{ + OathkeeperSvc: OathkeeperSvc, + OathkeeperSvcPort: OathkeeperSvcPort, + CorsConfig: TestCors, + AdditionalLabels: TestAdditionalLabels, + DefaultDomainName: DefaultDomain, + } +} + +func GetEmptyFakeClient() client.Client { + scheme := runtime.NewScheme() + err := networkingv1beta1.AddToScheme(scheme) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + err = rulev1alpha1.AddToScheme(scheme) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + err = securityv1beta1.AddToScheme(scheme) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + return fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() +} + +func GetRuleFor(path string, methods []string, mutators []*v1beta12.Mutator, accessStrategies []*v1beta12.Authenticator) v1beta12.Rule { + return v1beta12.Rule{ + Path: path, + Methods: methods, + Mutators: mutators, + AccessStrategies: accessStrategies, + } +} + +func GetRuleWithServiceFor(path string, methods []string, mutators []*v1beta12.Mutator, accessStrategies []*v1beta12.Authenticator, service *v1beta12.Service) v1beta12.Rule { + return v1beta12.Rule{ + Path: path, + Methods: methods, + Mutators: mutators, + AccessStrategies: accessStrategies, + Service: service, + } +} + +func GetAPIRuleFor(rules []v1beta12.Rule) *v1beta12.APIRule { + return &v1beta12.APIRule{ + ObjectMeta: v1.ObjectMeta{ + Name: ApiName, + UID: ApiUID, + Namespace: ApiNamespace, + }, + TypeMeta: v1.TypeMeta{ + APIVersion: ApiAPIVersion, + Kind: ApiKind, + }, + Spec: v1beta12.APIRuleSpec{ + Gateway: &ApiGateway, + Service: &v1beta12.Service{ + Name: &ServiceName, + Port: &ServicePort, + }, + Host: &ServiceHost, + Rules: rules, + }, + } +} + +func ToCSVList(input []string) string { + if len(input) == 0 { + return "" + } + + res := `"` + input[0] + `"` + + for i := 1; i < len(input); i++ { + res = res + "," + `"` + input[i] + `"` + } + + return res +} diff --git a/internal/processing/istio/authorization_policy_processor.go b/internal/processing/istio/authorization_policy_processor.go new file mode 100644 index 000000000..33f5779a3 --- /dev/null +++ b/internal/processing/istio/authorization_policy_processor.go @@ -0,0 +1,167 @@ +package istio + +import ( + "context" + "fmt" + + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/builders" + "github.com/kyma-incubator/api-gateway/internal/processing" + "istio.io/api/security/v1beta1" + securityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +// AuthorizationPolicyProcessor is the generic processor that handles the Istio Authorization Policies in the reconciliation of API Rule. +type AuthorizationPolicyProcessor struct { + Creator authorizationPolicyCreator +} + +// NewAuthorizationPolicyProcessor returns a AuthorizationPolicyProcessor with the desired state handling specific for the Istio handler. +func NewAuthorizationPolicyProcessor(config processing.ReconciliationConfig) AuthorizationPolicyProcessor { + return AuthorizationPolicyProcessor{ + Creator: authorizationPolicyCreator{ + additionalLabels: config.AdditionalLabels, + }, + } +} + +type authorizationPolicyCreator struct { + additionalLabels map[string]string +} + +// Create returns the Authorization Policy using the configuration of the APIRule. +func (r authorizationPolicyCreator) Create(api *gatewayv1beta1.APIRule) map[string]*securityv1beta1.AuthorizationPolicy { + pathDuplicates := processing.HasPathDuplicates(api.Spec.Rules) + authorizationPolicies := make(map[string]*securityv1beta1.AuthorizationPolicy) + for _, rule := range api.Spec.Rules { + if processing.IsSecured(rule) { + ar := generateAuthorizationPolicy(api, rule, r.additionalLabels) + authorizationPolicies[getAuthorizationPolicyKey(pathDuplicates, ar)] = ar + } + } + return authorizationPolicies +} + +func generateAuthorizationPolicy(api *gatewayv1beta1.APIRule, rule gatewayv1beta1.Rule, additionalLabels map[string]string) *securityv1beta1.AuthorizationPolicy { + namePrefix := fmt.Sprintf("%s-", api.ObjectMeta.Name) + namespace := api.ObjectMeta.Namespace + ownerRef := processing.GenerateOwnerRef(api) + + apBuilder := builders.AuthorizationPolicyBuilder(). + GenerateName(namePrefix). + Namespace(namespace). + Owner(builders.OwnerReference().From(&ownerRef)). + Spec(builders.AuthorizationPolicySpecBuilder().From(generateAuthorizationPolicySpec(api, rule))). + Label(processing.OwnerLabel, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)). + Label(processing.OwnerLabelv1alpha1, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)) + + for k, v := range additionalLabels { + apBuilder.Label(k, v) + } + + return apBuilder.Get() +} + +func generateAuthorizationPolicySpec(api *gatewayv1beta1.APIRule, rule gatewayv1beta1.Rule) *v1beta1.AuthorizationPolicy { + var serviceName string + if rule.Service != nil { + serviceName = *rule.Service.Name + } else { + serviceName = *api.Spec.Service.Name + } + + authorizationPolicySpec := builders.AuthorizationPolicySpecBuilder(). + Selector(builders.SelectorBuilder().MatchLabels("app", serviceName)). + Rule(builders.RuleBuilder(). + RuleFrom(builders.RuleFromBuilder().Source()). + RuleTo(builders.RuleToBuilder(). + Operation(builders.OperationBuilder().Methods(rule.Methods).Path(rule.Path)))) + + return authorizationPolicySpec.Get() +} + +func (r AuthorizationPolicyProcessor) EvaluateReconciliation(ctx context.Context, client ctrlclient.Client, apiRule *gatewayv1beta1.APIRule) ([]*processing.ObjectChange, error) { + desired := r.getDesiredState(apiRule) + actual, err := r.getActualState(ctx, client, apiRule) + if err != nil { + return make([]*processing.ObjectChange, 0), err + } + + changes := r.getObjectChanges(desired, actual) + + return changes, nil +} + +func (r AuthorizationPolicyProcessor) getDesiredState(api *gatewayv1beta1.APIRule) map[string]*securityv1beta1.AuthorizationPolicy { + return r.Creator.Create(api) +} + +func (r AuthorizationPolicyProcessor) getActualState(ctx context.Context, client ctrlclient.Client, api *gatewayv1beta1.APIRule) (map[string]*securityv1beta1.AuthorizationPolicy, error) { + labels := processing.GetOwnerLabels(api) + + var apList securityv1beta1.AuthorizationPolicyList + if err := client.List(ctx, &apList, ctrlclient.MatchingLabels(labels)); err != nil { + return nil, err + } + + authorizationPolicies := make(map[string]*securityv1beta1.AuthorizationPolicy) + pathDuplicates := processing.HasPathDuplicates(api.Spec.Rules) + for i := range apList.Items { + obj := apList.Items[i] + authorizationPolicies[getAuthorizationPolicyKey(pathDuplicates, obj)] = obj + } + + return authorizationPolicies, nil +} + +func (r AuthorizationPolicyProcessor) getObjectChanges(desiredAps map[string]*securityv1beta1.AuthorizationPolicy, actualAps map[string]*securityv1beta1.AuthorizationPolicy) []*processing.ObjectChange { + apChanges := make(map[string]*processing.ObjectChange) + + for path, rule := range desiredAps { + + if actualAps[path] != nil { + actualAps[path].Spec = *rule.Spec.DeepCopy() + apChanges[path] = processing.NewObjectUpdateAction(actualAps[path]) + } else { + apChanges[path] = processing.NewObjectCreateAction(rule) + } + + } + + for path, rule := range actualAps { + if desiredAps[path] == nil { + apChanges[path] = processing.NewObjectDeleteAction(rule) + } + } + + apChangesToApply := make([]*processing.ObjectChange, 0, len(apChanges)) + + for _, applyCommand := range apChanges { + apChangesToApply = append(apChangesToApply, applyCommand) + } + + return apChangesToApply +} + +func getAuthorizationPolicyKey(hasPathDuplicates bool, ap *securityv1beta1.AuthorizationPolicy) string { + key := "" + if ap.Spec.Rules != nil && len(ap.Spec.Rules) > 0 && ap.Spec.Rules[0].To != nil && len(ap.Spec.Rules[0].To) > 0 { + if hasPathDuplicates { + key = fmt.Sprintf("%s:%s", + sliceToString(ap.Spec.Rules[0].To[0].Operation.Paths), + sliceToString(ap.Spec.Rules[0].To[0].Operation.Methods)) + } else { + key = sliceToString(ap.Spec.Rules[0].To[0].Operation.Paths) + } + } + + return key +} + +func sliceToString(ss []string) (s string) { + for _, el := range ss { + s += el + } + return +} diff --git a/internal/processing/istio/authorization_policy_processor_test.go b/internal/processing/istio/authorization_policy_processor_test.go new file mode 100644 index 000000000..2aa899e90 --- /dev/null +++ b/internal/processing/istio/authorization_policy_processor_test.go @@ -0,0 +1,197 @@ +package istio_test + +import ( + "context" + "fmt" + + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + . "github.com/kyma-incubator/api-gateway/internal/processing/internal/test" + "github.com/kyma-incubator/api-gateway/internal/processing/istio" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + securityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ = Describe("Authorization Policy Processor", func() { + + createIstioJwtAccessStrategy := func() *gatewayv1beta1.Authenticator { + jwtConfigJSON := fmt.Sprintf(`{ + "authentications": [{"issuer": "%s", "jwksUri": "%s"}]}`, JwtIssuer, JwksUri) + return &gatewayv1beta1.Authenticator{ + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + } + } + It("should produce two APs for a rule with one issuer and two paths", func() { + // given + jwt := createIstioJwtAccessStrategy() + service := &gatewayv1beta1.Service{ + Name: &ServiceName, + Port: &ServicePort, + } + + ruleJwt := GetRuleWithServiceFor(HeadersApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}, service) + ruleJwt2 := GetRuleWithServiceFor(ImgApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}, service) + apiRule := GetAPIRuleFor([]gatewayv1beta1.Rule{ruleJwt, ruleJwt2}) + client := GetEmptyFakeClient() + processor := istio.NewAuthorizationPolicyProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + ap1 := result[0].Obj.(*securityv1beta1.AuthorizationPolicy) + ap2 := result[1].Obj.(*securityv1beta1.AuthorizationPolicy) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(2)) + + Expect(ap1).NotTo(BeNil()) + Expect(ap1.ObjectMeta.Name).To(BeEmpty()) + Expect(ap1.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(ap1.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(ap1.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(ap1.Spec.Selector.MatchLabels[TestSelectorKey]).NotTo(BeNil()) + Expect(ap1.Spec.Selector.MatchLabels[TestSelectorKey]).To(Equal(ServiceName)) + Expect(len(ap1.Spec.Rules)).To(Equal(1)) + Expect(len(ap1.Spec.Rules[0].From)).To(Equal(1)) + Expect(len(ap1.Spec.Rules[0].From[0].Source.RequestPrincipals)).To(Equal(1)) + Expect(ap1.Spec.Rules[0].From[0].Source.RequestPrincipals[0]).To(Equal("*")) + Expect(len(ap1.Spec.Rules[0].To)).To(Equal(1)) + Expect(len(ap1.Spec.Rules[0].To[0].Operation.Methods)).To(Equal(1)) + Expect(ap1.Spec.Rules[0].To[0].Operation.Methods).To(ContainElements(ApiMethods)) + Expect(len(ap1.Spec.Rules[0].To[0].Operation.Paths)).To(Equal(1)) + + Expect(len(ap1.OwnerReferences)).To(Equal(1)) + Expect(ap1.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(ap1.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(ap1.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(ap1.OwnerReferences[0].UID).To(Equal(ApiUID)) + + Expect(ap2).NotTo(BeNil()) + Expect(ap2.ObjectMeta.Name).To(BeEmpty()) + Expect(ap2.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(ap2.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(ap2.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(ap2.Spec.Selector.MatchLabels[TestSelectorKey]).NotTo(BeNil()) + Expect(ap2.Spec.Selector.MatchLabels[TestSelectorKey]).To(Equal(ServiceName)) + Expect(len(ap2.Spec.Rules)).To(Equal(1)) + Expect(len(ap2.Spec.Rules[0].From)).To(Equal(1)) + Expect(len(ap2.Spec.Rules[0].From[0].Source.RequestPrincipals)).To(Equal(1)) + Expect(ap2.Spec.Rules[0].From[0].Source.RequestPrincipals[0]).To(Equal("*")) + Expect(len(ap2.Spec.Rules[0].To)).To(Equal(1)) + Expect(len(ap2.Spec.Rules[0].To[0].Operation.Methods)).To(Equal(1)) + Expect(ap2.Spec.Rules[0].To[0].Operation.Methods).To(ContainElements(ApiMethods)) + Expect(len(ap2.Spec.Rules[0].To[0].Operation.Paths)).To(Equal(1)) + + Expect(ap2.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(ap2.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(ap2.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(ap2.OwnerReferences[0].UID).To(Equal(ApiUID)) + }) + + It("should produce one AP for a Rule without service, but service definition on ApiRule level", func() { + // given + jwt := createIstioJwtAccessStrategy() + client := GetEmptyFakeClient() + ruleJwt := GetRuleFor(HeadersApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}) + apiRule := GetAPIRuleFor([]gatewayv1beta1.Rule{ruleJwt}) + processor := istio.NewAuthorizationPolicyProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + ap := result[0].Obj.(*securityv1beta1.AuthorizationPolicy) + Expect(ap).NotTo(BeNil()) + Expect(ap.Spec.Selector.MatchLabels[TestSelectorKey]).To(Equal(ServiceName)) + }) + + It("should produce AP with service from Rule, when service is configured on Rule and ApiRule level", func() { + // given + jwt := createIstioJwtAccessStrategy() + ruleServiceName := "rule-scope-example-service" + service := &gatewayv1beta1.Service{ + Name: &ruleServiceName, + Port: &ServicePort, + } + client := GetEmptyFakeClient() + ruleJwt := GetRuleWithServiceFor(HeadersApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}, service) + apiRule := GetAPIRuleFor([]gatewayv1beta1.Rule{ruleJwt}) + + processor := istio.NewAuthorizationPolicyProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + ap := result[0].Obj.(*securityv1beta1.AuthorizationPolicy) + Expect(ap).NotTo(BeNil()) + Expect(ap.Spec.Selector.MatchLabels[TestSelectorKey]).To(Equal(ruleServiceName)) + }) + + It("should produce AP from a rule with two issuers and one path", func() { + jwtConfigJSON := fmt.Sprintf(`{ + "authentications": [{"issuer": "%s", "jwksUri": "%s"}, {"issuer": "%s", "jwksUri": "%s"}] + }`, JwtIssuer, JwksUri, JwtIssuer2, JwksUri2) + jwt := &gatewayv1beta1.Authenticator{ + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + } + client := GetEmptyFakeClient() + service := &gatewayv1beta1.Service{ + Name: &ServiceName, + Port: &ServicePort, + } + ruleJwt := GetRuleWithServiceFor(HeadersApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}, service) + apiRule := GetAPIRuleFor([]gatewayv1beta1.Rule{ruleJwt}) + processor := istio.NewAuthorizationPolicyProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + ap := result[0].Obj.(*securityv1beta1.AuthorizationPolicy) + + Expect(ap).NotTo(BeNil()) + Expect(ap.ObjectMeta.Name).To(BeEmpty()) + Expect(ap.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(ap.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(ap.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(ap.Spec.Selector.MatchLabels[TestSelectorKey]).NotTo(BeNil()) + Expect(ap.Spec.Selector.MatchLabels[TestSelectorKey]).To(Equal(ServiceName)) + Expect(len(ap.Spec.Rules)).To(Equal(1)) + Expect(len(ap.Spec.Rules[0].From)).To(Equal(1)) + Expect(len(ap.Spec.Rules[0].From[0].Source.RequestPrincipals)).To(Equal(1)) + Expect(ap.Spec.Rules[0].From[0].Source.RequestPrincipals[0]).To(Equal("*")) + Expect(len(ap.Spec.Rules[0].To)).To(Equal(1)) + Expect(len(ap.Spec.Rules[0].To[0].Operation.Methods)).To(Equal(1)) + Expect(ap.Spec.Rules[0].To[0].Operation.Methods).To(ContainElements(ApiMethods)) + Expect(len(ap.Spec.Rules[0].To[0].Operation.Paths)).To(Equal(1)) + Expect(ap.Spec.Rules[0].To[0].Operation.Paths).To(ContainElements(HeadersApiPath)) + + Expect(ap.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(ap.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(ap.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(ap.OwnerReferences[0].UID).To(Equal(ApiUID)) + }) +}) diff --git a/internal/processing/istio/istio_suite_test.go b/internal/processing/istio/istio_suite_test.go new file mode 100644 index 000000000..f86e29516 --- /dev/null +++ b/internal/processing/istio/istio_suite_test.go @@ -0,0 +1,15 @@ +package istio + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/envtest/printer" +) + +func TestIstio(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecsWithDefaultAndCustomReporters(t, "Istio Suite", + []Reporter{printer.NewProwReporter("api-gateway-istio-testsuite")}) +} diff --git a/internal/processing/istio/jwt_validator.go b/internal/processing/istio/jwt_validator.go new file mode 100644 index 000000000..777d377c2 --- /dev/null +++ b/internal/processing/istio/jwt_validator.go @@ -0,0 +1,53 @@ +package istio + +import ( + "encoding/json" + "fmt" + + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + istiojwt "github.com/kyma-incubator/api-gateway/internal/types/istio" + "github.com/kyma-incubator/api-gateway/internal/validation" +) + +type jwtValidator struct{} + +func (o *jwtValidator) Validate(attributePath string, handler *gatewayv1beta1.Handler) []validation.Failure { + var problems []validation.Failure + var template istiojwt.JwtConfig + + if !validation.ConfigNotEmpty(handler.Config) { + problems = append(problems, validation.Failure{AttributePath: attributePath + ".config", Message: "supplied config cannot be empty"}) + return problems + } + + err := json.Unmarshal(handler.Config.Raw, &template) + if err != nil { + problems = append(problems, validation.Failure{AttributePath: attributePath + ".config", Message: "Can't read json: " + err.Error()}) + return problems + } + + for i, auth := range template.Authentications { + invalidIssuer, err := validation.IsInvalidURL(auth.Issuer) + if invalidIssuer { + attrPath := fmt.Sprintf("%s%s[%d]%s", attributePath, ".config.authentications", i, ".issuer") + problems = append(problems, validation.Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is empty or not a valid url err=%s", err)}) + } + unsecuredIssuer, err := validation.IsUnsecuredURL(auth.Issuer) + if unsecuredIssuer { + attrPath := fmt.Sprintf("%s%s[%d]%s", attributePath, ".config.authentications", i, ".issuer") + problems = append(problems, validation.Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is not a secured url err=%s", err)}) + } + invalidJwksUri, err := validation.IsInvalidURL(auth.JwksUri) + if invalidJwksUri { + attrPath := fmt.Sprintf("%s%s[%d]%s", attributePath, ".config.authentications", i, ".jwksUri") + problems = append(problems, validation.Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is empty or not a valid url err=%s", err)}) + } + unsecuredJwksUri, err := validation.IsUnsecuredURL(auth.JwksUri) + if unsecuredJwksUri { + attrPath := fmt.Sprintf("%s%s[%d]%s", attributePath, ".config.authentications", i, ".jwksUri") + problems = append(problems, validation.Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is not a secured url err=%s", err)}) + } + } + + return problems +} diff --git a/internal/processing/istio/jwt_validator_test.go b/internal/processing/istio/jwt_validator_test.go new file mode 100644 index 000000000..b3c1c1384 --- /dev/null +++ b/internal/processing/istio/jwt_validator_test.go @@ -0,0 +1,129 @@ +package istio + +import ( + "encoding/json" + + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + istiojwt "github.com/kyma-incubator/api-gateway/internal/types/istio" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ = Describe("JWT Validator", func() { + + It("Should fail with empty config", func() { + //given + handler := &gatewayv1beta1.Handler{Name: "jwt", Config: emptyJWTIstioConfig()} + + //when + problems := (&jwtValidator{}).Validate("some.attribute", handler) + + //then + Expect(problems).To(HaveLen(1)) + Expect(problems[0].AttributePath).To(Equal("some.attribute.config")) + Expect(problems[0].Message).To(Equal("supplied config cannot be empty")) + }) + + It("Should fail for config with invalid trustedIssuers and JWKSUrls", func() { + //given + handler := &gatewayv1beta1.Handler{Name: "jwt", Config: simpleJWTIstioConfig("a t g o")} + + //when + problems := (&jwtValidator{}).Validate("some.attribute", handler) + + //then + Expect(problems).To(HaveLen(2)) + Expect(problems[0].AttributePath).To(Equal("some.attribute.config.authentications[0].issuer")) + Expect(problems[0].Message).To(ContainSubstring("value is empty or not a valid url")) + Expect(problems[1].AttributePath).To(Equal("some.attribute.config.authentications[0].jwksUri")) + Expect(problems[1].Message).To(ContainSubstring("value is empty or not a valid url")) + }) + + It("Should fail for config with plain HTTP JWKSUrls and trustedIssuers", func() { + //given + handler := &gatewayv1beta1.Handler{Name: "jwt", Config: testURLJWTIstioConfig("http://issuer.test/.well-known/jwks.json", "http://issuer.test/")} + + //when + problems := (&jwtValidator{}).Validate("some.attribute", handler) + + //then + Expect(problems).To(HaveLen(2)) + Expect(problems[0].AttributePath).To(Equal("some.attribute.config.authentications[0].issuer")) + Expect(problems[0].Message).To(ContainSubstring("value is not a secured url")) + Expect(problems[1].AttributePath).To(Equal("some.attribute.config.authentications[0].jwksUri")) + Expect(problems[1].Message).To(ContainSubstring("value is not a secured url")) + }) + + It("Should succeed for config with file JWKSUrls and HTTPS trustedIssuers", func() { + //given + handler := &gatewayv1beta1.Handler{Name: "jwt", Config: testURLJWTIstioConfig("file://.well-known/jwks.json", "https://issuer.test/")} + + //when + problems := (&jwtValidator{}).Validate("some.attribute", handler) + + //then + Expect(problems).To(HaveLen(0)) + }) + + It("Should succeed for config with HTTPS JWKSUrls and trustedIssuers", func() { + //given + handler := &gatewayv1beta1.Handler{Name: "jwt", Config: testURLJWTIstioConfig("https://issuer.test/.well-known/jwks.json", "https://issuer.test/")} + + //when + problems := (&jwtValidator{}).Validate("some.attribute", handler) + + //then + Expect(problems).To(HaveLen(0)) + }) + + It("Should fail for invalid JSON", func() { + //given + handler := &gatewayv1beta1.Handler{Name: "jwt", Config: &runtime.RawExtension{Raw: []byte("/abc]")}} + + //when + problems := (&jwtValidator{}).Validate("some.attribute", handler) + + //then + Expect(problems).To(HaveLen(1)) + Expect(problems[0].AttributePath).To(Equal("some.attribute.config")) + Expect(problems[0].Message).To(Equal("Can't read json: invalid character '/' looking for beginning of value")) + }) +}) + +func emptyJWTIstioConfig() *runtime.RawExtension { + return getRawConfig( + &istiojwt.JwtConfig{}) +} + +func simpleJWTIstioConfig(trustedIssuers ...string) *runtime.RawExtension { + issuers := []istiojwt.JwtAuth{} + for _, issuer := range trustedIssuers { + issuers = append(issuers, istiojwt.JwtAuth{ + Issuer: issuer, + JwksUri: issuer, + }) + } + jwtConfig := istiojwt.JwtConfig{Authentications: issuers} + return getRawConfig(jwtConfig) +} + +func testURLJWTIstioConfig(JWKSUrl string, trustedIssuer string) *runtime.RawExtension { + return getRawConfig( + istiojwt.JwtConfig{ + Authentications: []istiojwt.JwtAuth{ + { + Issuer: trustedIssuer, + JwksUri: JWKSUrl, + }, + }, + }) +} + +func getRawConfig(config any) *runtime.RawExtension { + bytes, err := json.Marshal(config) + Expect(err).To(BeNil()) + return &runtime.RawExtension{ + Raw: bytes, + } +} diff --git a/internal/processing/istio/reconciliation.go b/internal/processing/istio/reconciliation.go new file mode 100644 index 000000000..13eef6be6 --- /dev/null +++ b/internal/processing/istio/reconciliation.go @@ -0,0 +1,48 @@ +package istio + +import ( + "context" + + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/processing" + "github.com/kyma-incubator/api-gateway/internal/validation" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Reconciliation struct { + processors []processing.ReconciliationProcessor + config processing.ReconciliationConfig +} + +func NewIstioReconciliation(config processing.ReconciliationConfig) Reconciliation { + vsProcessor := NewVirtualServiceProcessor(config) + apProcessor := NewAuthorizationPolicyProcessor(config) + raProcessor := NewRequestAuthenticationProcessor(config) + + return Reconciliation{ + processors: []processing.ReconciliationProcessor{vsProcessor, raProcessor, apProcessor}, + config: config, + } +} + +func (r Reconciliation) Validate(ctx context.Context, client client.Client, apiRule *gatewayv1beta1.APIRule) ([]validation.Failure, error) { + + var vsList networkingv1beta1.VirtualServiceList + if err := client.List(ctx, &vsList); err != nil { + return make([]validation.Failure, 0), err + } + + validator := validation.APIRule{ + JwtValidator: &jwtValidator{}, + ServiceBlockList: r.config.ServiceBlockList, + DomainAllowList: r.config.DomainAllowList, + HostBlockList: r.config.HostBlockList, + DefaultDomainName: r.config.DefaultDomainName, + } + return validator.Validate(apiRule, vsList), nil +} + +func (r Reconciliation) GetProcessors() []processing.ReconciliationProcessor { + return r.processors +} diff --git a/internal/processing/istio/request_authentication_processor.go b/internal/processing/istio/request_authentication_processor.go new file mode 100644 index 000000000..93bc643b4 --- /dev/null +++ b/internal/processing/istio/request_authentication_processor.go @@ -0,0 +1,150 @@ +package istio + +import ( + "context" + "fmt" + + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/builders" + "github.com/kyma-incubator/api-gateway/internal/processing" + "istio.io/api/security/v1beta1" + securityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +// RequestAuthenticationProcessor is the generic processor that handles the Istio Request Authentications in the reconciliation of API Rule. +type RequestAuthenticationProcessor struct { + Creator requestAuthenticationCreator +} + +// NewRequestAuthenticationProcessor returns a RequestAuthenticationProcessor with the desired state handling specific for the Istio handler. +func NewRequestAuthenticationProcessor(config processing.ReconciliationConfig) RequestAuthenticationProcessor { + return RequestAuthenticationProcessor{ + Creator: requestAuthenticationCreator{ + additionalLabels: config.AdditionalLabels, + }, + } +} + +type requestAuthenticationCreator struct { + additionalLabels map[string]string +} + +// Create returns the Virtual Service using the configuration of the APIRule. +func (r requestAuthenticationCreator) Create(api *gatewayv1beta1.APIRule) map[string]*securityv1beta1.RequestAuthentication { + requestAuthentications := make(map[string]*securityv1beta1.RequestAuthentication) + for _, rule := range api.Spec.Rules { + if processing.IsSecured(rule) { + ra := generateRequestAuthentication(api, rule, r.additionalLabels) + requestAuthentications[getRequestAuthenticationKey(ra)] = ra + } + } + return requestAuthentications +} + +func generateRequestAuthentication(api *gatewayv1beta1.APIRule, rule gatewayv1beta1.Rule, additionalLabels map[string]string) *securityv1beta1.RequestAuthentication { + namePrefix := fmt.Sprintf("%s-", api.ObjectMeta.Name) + namespace := api.ObjectMeta.Namespace + ownerRef := processing.GenerateOwnerRef(api) + + raBuilder := builders.RequestAuthenticationBuilder(). + GenerateName(namePrefix). + Namespace(namespace). + Owner(builders.OwnerReference().From(&ownerRef)). + Spec(builders.RequestAuthenticationSpecBuilder().From(generateRequestAuthenticationSpec(api, rule))). + Label(processing.OwnerLabel, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)). + Label(processing.OwnerLabelv1alpha1, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)) + + for k, v := range additionalLabels { + raBuilder.Label(k, v) + } + + return raBuilder.Get() +} + +func generateRequestAuthenticationSpec(api *gatewayv1beta1.APIRule, rule gatewayv1beta1.Rule) *v1beta1.RequestAuthentication { + + var serviceName string + if rule.Service != nil { + serviceName = *rule.Service.Name + } else { + serviceName = *api.Spec.Service.Name + } + + requestAuthenticationSpec := builders.RequestAuthenticationSpecBuilder(). + Selector(builders.SelectorBuilder().MatchLabels("app", serviceName)). + JwtRules(builders.JwtRuleBuilder().From(rule.AccessStrategies)) + + return requestAuthenticationSpec.Get() +} + +func (r RequestAuthenticationProcessor) EvaluateReconciliation(ctx context.Context, client ctrlclient.Client, apiRule *gatewayv1beta1.APIRule) ([]*processing.ObjectChange, error) { + desired := r.getDesiredState(apiRule) + actual, err := r.getActualState(ctx, client, apiRule) + if err != nil { + return make([]*processing.ObjectChange, 0), err + } + + changes := r.getObjectChanges(desired, actual) + + return changes, nil +} + +func (r RequestAuthenticationProcessor) getDesiredState(api *gatewayv1beta1.APIRule) map[string]*securityv1beta1.RequestAuthentication { + return r.Creator.Create(api) +} + +func (r RequestAuthenticationProcessor) getActualState(ctx context.Context, client ctrlclient.Client, api *gatewayv1beta1.APIRule) (map[string]*securityv1beta1.RequestAuthentication, error) { + labels := processing.GetOwnerLabels(api) + + var raList securityv1beta1.RequestAuthenticationList + if err := client.List(ctx, &raList, ctrlclient.MatchingLabels(labels)); err != nil { + return nil, err + } + + requestAuthentications := make(map[string]*securityv1beta1.RequestAuthentication) + + for i := range raList.Items { + obj := raList.Items[i] + requestAuthentications[getRequestAuthenticationKey(obj)] = obj + } + + return requestAuthentications, nil +} + +func (r RequestAuthenticationProcessor) getObjectChanges(desiredRas map[string]*securityv1beta1.RequestAuthentication, actualRas map[string]*securityv1beta1.RequestAuthentication) []*processing.ObjectChange { + raChanges := make(map[string]*processing.ObjectChange) + + for path, rule := range desiredRas { + + if actualRas[path] != nil { + actualRas[path].Spec = *rule.Spec.DeepCopy() + raChanges[path] = processing.NewObjectUpdateAction(actualRas[path]) + } else { + raChanges[path] = processing.NewObjectCreateAction(rule) + } + + } + + for path, rule := range actualRas { + if desiredRas[path] == nil { + raChanges[path] = processing.NewObjectDeleteAction(rule) + } + } + + raChangesToApply := make([]*processing.ObjectChange, 0, len(raChanges)) + + for _, applyCommand := range raChanges { + raChangesToApply = append(raChangesToApply, applyCommand) + } + + return raChangesToApply +} + +func getRequestAuthenticationKey(ra *securityv1beta1.RequestAuthentication) string { + key := "" + for _, k := range ra.Spec.JwtRules { + key += fmt.Sprintf("%s:%s", k.Issuer, k.JwksUri) + } + return key +} diff --git a/internal/processing/istio/request_authentication_processor_test.go b/internal/processing/istio/request_authentication_processor_test.go new file mode 100644 index 000000000..24ab3c5c2 --- /dev/null +++ b/internal/processing/istio/request_authentication_processor_test.go @@ -0,0 +1,165 @@ +package istio_test + +import ( + "context" + "fmt" + + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + . "github.com/kyma-incubator/api-gateway/internal/processing/internal/test" + "github.com/kyma-incubator/api-gateway/internal/processing/istio" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + securityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ = Describe("Request Authentication Processor", func() { + + createIstioJwtAccessStrategy := func() *gatewayv1beta1.Authenticator { + jwtConfigJSON := fmt.Sprintf(`{ + "authentications": [{"issuer": "%s", "jwksUri": "%s"}]}`, JwtIssuer, JwksUri) + return &gatewayv1beta1.Authenticator{ + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + } + } + + It("should produce one RA for a rule with one issuer and two paths", func() { + // given + jwt := createIstioJwtAccessStrategy() + service := &gatewayv1beta1.Service{ + Name: &ServiceName, + Port: &ServicePort, + } + + ruleJwt := GetRuleWithServiceFor(HeadersApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}, service) + ruleJwt2 := GetRuleWithServiceFor(ImgApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}, service) + apiRule := GetAPIRuleFor([]gatewayv1beta1.Rule{ruleJwt, ruleJwt2}) + client := GetEmptyFakeClient() + processor := istio.NewRequestAuthenticationProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + ra := result[0].Obj.(*securityv1beta1.RequestAuthentication) + Expect(ra).NotTo(BeNil()) + Expect(ra.ObjectMeta.Name).To(BeEmpty()) + Expect(ra.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(ra.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(ra.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(len(ra.OwnerReferences)).To(Equal(1)) + Expect(ra.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(ra.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(ra.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(ra.OwnerReferences[0].UID).To(Equal(ApiUID)) + + Expect(ra.Spec.Selector.MatchLabels[TestSelectorKey]).NotTo(BeNil()) + Expect(ra.Spec.Selector.MatchLabels[TestSelectorKey]).To(Equal(ServiceName)) + Expect(len(ra.Spec.JwtRules)).To(Equal(1)) + Expect(ra.Spec.JwtRules[0].Issuer).To(Equal(JwtIssuer)) + Expect(ra.Spec.JwtRules[0].JwksUri).To(Equal(JwksUri)) + }) + + It("should produce RA for a Rule without service, but service definition on ApiRule level", func() { + // given + jwt := createIstioJwtAccessStrategy() + client := GetEmptyFakeClient() + ruleJwt := GetRuleFor(HeadersApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}) + apiRule := GetAPIRuleFor([]gatewayv1beta1.Rule{ruleJwt}) + processor := istio.NewRequestAuthenticationProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + ra := result[0].Obj.(*securityv1beta1.RequestAuthentication) + Expect(ra).NotTo(BeNil()) + Expect(ra.Spec.Selector.MatchLabels[TestSelectorKey]).To(Equal(ServiceName)) + }) + + It("should produce RA with service from Rule, when service is configured on Rule and ApiRule level", func() { + // given + jwt := createIstioJwtAccessStrategy() + ruleServiceName := "rule-scope-example-service" + service := &gatewayv1beta1.Service{ + Name: &ruleServiceName, + Port: &ServicePort, + } + client := GetEmptyFakeClient() + ruleJwt := GetRuleWithServiceFor(HeadersApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}, service) + apiRule := GetAPIRuleFor([]gatewayv1beta1.Rule{ruleJwt}) + + processor := istio.NewRequestAuthenticationProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + ra := result[0].Obj.(*securityv1beta1.RequestAuthentication) + Expect(ra).NotTo(BeNil()) + Expect(ra.Spec.Selector.MatchLabels[TestSelectorKey]).To(Equal(ruleServiceName)) + }) + It("should produce RA from a rule with two issuers and one path", func() { + jwtConfigJSON := fmt.Sprintf(`{ + "authentications": [{"issuer": "%s", "jwksUri": "%s"}, {"issuer": "%s", "jwksUri": "%s"}] + }`, JwtIssuer, JwksUri, JwtIssuer2, JwksUri2) + jwt := &gatewayv1beta1.Authenticator{ + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + } + client := GetEmptyFakeClient() + service := &gatewayv1beta1.Service{ + Name: &ServiceName, + Port: &ServicePort, + } + ruleJwt := GetRuleWithServiceFor(HeadersApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}, service) + apiRule := GetAPIRuleFor([]gatewayv1beta1.Rule{ruleJwt}) + processor := istio.NewRequestAuthenticationProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + ra := result[0].Obj.(*securityv1beta1.RequestAuthentication) + + Expect(ra).NotTo(BeNil()) + Expect(ra.ObjectMeta.Name).To(BeEmpty()) + Expect(ra.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(ra.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(ra.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(len(ra.OwnerReferences)).To(Equal(1)) + Expect(ra.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(ra.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(ra.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(ra.OwnerReferences[0].UID).To(Equal(ApiUID)) + + Expect(ra.Spec.Selector.MatchLabels[TestSelectorKey]).NotTo(BeNil()) + Expect(ra.Spec.Selector.MatchLabels[TestSelectorKey]).To(Equal(ServiceName)) + Expect(len(ra.Spec.JwtRules)).To(Equal(2)) + Expect(ra.Spec.JwtRules[0].Issuer).To(Equal(JwtIssuer)) + Expect(ra.Spec.JwtRules[0].JwksUri).To(Equal(JwksUri)) + Expect(ra.Spec.JwtRules[1].Issuer).To(Equal(JwtIssuer2)) + Expect(ra.Spec.JwtRules[1].JwksUri).To(Equal(JwksUri2)) + }) +}) diff --git a/internal/processing/istio/virtual_service_processor.go b/internal/processing/istio/virtual_service_processor.go new file mode 100644 index 000000000..17cd5f9fe --- /dev/null +++ b/internal/processing/istio/virtual_service_processor.go @@ -0,0 +1,111 @@ +package istio + +import ( + "fmt" + + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/builders" + "github.com/kyma-incubator/api-gateway/internal/helpers" + "github.com/kyma-incubator/api-gateway/internal/processing" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" +) + +// NewVirtualServiceProcessor returns a VirtualServiceProcessor with the desired state handling specific for the Istio handler. +func NewVirtualServiceProcessor(config processing.ReconciliationConfig) processing.VirtualServiceProcessor { + return processing.VirtualServiceProcessor{ + Creator: virtualServiceCreator{ + oathkeeperSvc: config.OathkeeperSvc, + oathkeeperSvcPort: config.OathkeeperSvcPort, + corsConfig: config.CorsConfig, + additionalLabels: config.AdditionalLabels, + defaultDomainName: config.DefaultDomainName, + }, + } +} + +type virtualServiceCreator struct { + oathkeeperSvc string + oathkeeperSvcPort uint32 + corsConfig *processing.CorsConfig + defaultDomainName string + additionalLabels map[string]string +} + +// Create returns the Virtual Service using the configuration of the APIRule. +func (r virtualServiceCreator) Create(api *gatewayv1beta1.APIRule) *networkingv1beta1.VirtualService { + virtualServiceNamePrefix := fmt.Sprintf("%s-", api.ObjectMeta.Name) + ownerRef := processing.GenerateOwnerRef(api) + + vsSpecBuilder := builders.VirtualServiceSpec() + vsSpecBuilder.Host(helpers.GetHostWithDomain(*api.Spec.Host, r.defaultDomainName)) + vsSpecBuilder.Gateway(*api.Spec.Gateway) + filteredRules := filterDuplicatePaths(api.Spec.Rules) + + for _, rule := range filteredRules { + httpRouteBuilder := builders.HTTPRoute() + serviceNamespace := helpers.FindServiceNamespace(api, &rule) + routeDirectlyToService := false + if !processing.IsSecured(rule) { + routeDirectlyToService = true + } else if processing.IsJwtSecured(rule) { + routeDirectlyToService = true + } + + var host string + var port uint32 + + if routeDirectlyToService { + // Use rule level service if it exists + if rule.Service != nil { + host = helpers.GetHostLocalDomain(*rule.Service.Name, *serviceNamespace) + port = *rule.Service.Port + } else { + // Otherwise use service defined on APIRule spec level + host = helpers.GetHostLocalDomain(*api.Spec.Service.Name, *serviceNamespace) + port = *api.Spec.Service.Port + } + } else { + host = r.oathkeeperSvc + port = r.oathkeeperSvcPort + } + + httpRouteBuilder.Route(builders.RouteDestination().Host(host).Port(port)) + httpRouteBuilder.Match(builders.MatchRequest().Uri().Regex(rule.Path)) + httpRouteBuilder.CorsPolicy(builders.CorsPolicy(). + AllowOrigins(r.corsConfig.AllowOrigins...). + AllowMethods(r.corsConfig.AllowMethods...). + AllowHeaders(r.corsConfig.AllowHeaders...)) + httpRouteBuilder.Headers(builders.Headers(). + SetHostHeader(helpers.GetHostWithDomain(*api.Spec.Host, r.defaultDomainName))) + vsSpecBuilder.HTTP(httpRouteBuilder) + + } + + vsBuilder := builders.VirtualService(). + GenerateName(virtualServiceNamePrefix). + Namespace(api.ObjectMeta.Namespace). + Owner(builders.OwnerReference().From(&ownerRef)). + Label(processing.OwnerLabel, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)). + Label(processing.OwnerLabelv1alpha1, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)) + + for k, v := range r.additionalLabels { + vsBuilder.Label(k, v) + } + + vsBuilder.Spec(vsSpecBuilder) + + return vsBuilder.Get() +} + +func filterDuplicatePaths(rules []gatewayv1beta1.Rule) []gatewayv1beta1.Rule { + duplicates := make(map[string]bool) + var filteredRules []gatewayv1beta1.Rule + for _, rule := range rules { + if _, exists := duplicates[rule.Path]; !exists { + duplicates[rule.Path] = true + filteredRules = append(filteredRules, rule) + } + } + + return filteredRules +} diff --git a/internal/processing/istio/virtual_service_processor_test.go b/internal/processing/istio/virtual_service_processor_test.go new file mode 100644 index 000000000..05841f611 --- /dev/null +++ b/internal/processing/istio/virtual_service_processor_test.go @@ -0,0 +1,662 @@ +package istio_test + +import ( + "context" + "fmt" + + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/processing" + . "github.com/kyma-incubator/api-gateway/internal/processing/internal/test" + "github.com/kyma-incubator/api-gateway/internal/processing/istio" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + rulev1alpha1 "github.com/ory/oathkeeper-maester/api/v1alpha1" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var _ = Describe("Virtual Service Processor", func() { + When("handler is allow", func() { + It("should create for allow authenticator", func() { + // given + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "allow", + }, + }, + } + + allowRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := istio.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Action.String()).To(Equal("create")) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(vs).NotTo(BeNil()) + Expect(len(vs.Spec.Gateways)).To(Equal(1)) + Expect(len(vs.Spec.Hosts)).To(Equal(1)) + Expect(vs.Spec.Hosts[0]).To(Equal(ServiceHost)) + Expect(len(vs.Spec.Http)).To(Equal(1)) + + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(ServiceName + "." + ApiNamespace + ".svc.cluster.local")) + Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(ServicePort)) + + Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) + Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) + + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + + Expect(vs.ObjectMeta.Name).To(BeEmpty()) + Expect(vs.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(vs.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(vs.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(ApiUID)) + }) + + It("should override destination host for specified spec level service namespace", func() { + // given + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "allow", + }, + }, + } + + allowRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + + overrideServiceName := "testName" + overrideServiceNamespace := "testName-namespace" + overrideServicePort := uint32(8080) + + apiRule.Spec.Service = &gatewayv1beta1.Service{ + Name: &overrideServiceName, + Namespace: &overrideServiceNamespace, + Port: &overrideServicePort, + } + client := GetEmptyFakeClient() + processor := istio.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(overrideServiceName + "." + overrideServiceNamespace + ".svc.cluster.local")) + }) + + It("should override destination host with rule level service namespace", func() { + // given + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "allow", + }, + }, + } + + overrideServiceName := "testName" + overrideServiceNamespace := "testName-namespace" + overrideServicePort := uint32(8080) + + service := &gatewayv1beta1.Service{ + Name: &overrideServiceName, + Namespace: &overrideServiceNamespace, + Port: &overrideServicePort, + } + + allowRule := GetRuleWithServiceFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies, service) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := istio.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + //verify VS has rule level destination host + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(overrideServiceName + "." + overrideServiceNamespace + ".svc.cluster.local")) + + }) + It("should return VS with default domain name when the hostname does not contain domain name", func() { + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "allow", + }, + }, + } + + allowRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + apiRule.Spec.Host = &ServiceHostWithNoDomain + client := GetEmptyFakeClient() + processor := istio.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + //verify VS + Expect(vs).NotTo(BeNil()) + Expect(len(vs.Spec.Hosts)).To(Equal(1)) + Expect(vs.Spec.Hosts[0]).To(Equal(ServiceHost)) + + }) + }) + + When("handler is noop", func() { + It("should not override Oathkeeper service destination host with spec level service", func() { + // given + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + overrideServiceName := "testName" + overrideServicePort := uint32(8080) + + service := &gatewayv1beta1.Service{ + Name: &overrideServiceName, + Port: &overrideServicePort, + } + + allowRule := GetRuleWithServiceFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies, service) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := istio.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(OathkeeperSvc)) + }) + When("existing virtual service has owner v1alpha1 owner label", func() { + It("should get and update", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + noopRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, noop) + rules := []gatewayv1beta1.Rule{noopRule} + + apiRule := GetAPIRuleFor(rules) + + rule := rulev1alpha1.Rule{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + processing.OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), + }, + }, + Spec: rulev1alpha1.RuleSpec{ + Match: &rulev1alpha1.Match{ + URL: "some url", + }, + }, + } + + vs := networkingv1beta1.VirtualService{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + processing.OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), + }, + }, + } + + scheme := runtime.NewScheme() + err := rulev1alpha1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + err = networkingv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + err = gatewayv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&rule, &vs).Build() + processor := istio.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Action.String()).To(Equal("update")) + + resultVs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(resultVs).NotTo(BeNil()) + Expect(resultVs).NotTo(BeNil()) + Expect(len(resultVs.Spec.Gateways)).To(Equal(1)) + Expect(len(resultVs.Spec.Hosts)).To(Equal(1)) + Expect(resultVs.Spec.Hosts[0]).To(Equal(ServiceHost)) + Expect(len(resultVs.Spec.Http)).To(Equal(1)) + + Expect(len(resultVs.Spec.Http[0].Route)).To(Equal(1)) + Expect(resultVs.Spec.Http[0].Route[0].Destination.Host).To(Equal(OathkeeperSvc)) + Expect(resultVs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(OathkeeperSvcPort)) + + Expect(len(resultVs.Spec.Http[0].Match)).To(Equal(1)) + Expect(resultVs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) + + Expect(resultVs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(resultVs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(resultVs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + }) + }) + }) + + When("multiple handler", func() { + It("should return service for given paths", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + jwtConfigJSON := fmt.Sprintf(` + { + "trusted_issuers": ["%s"], + "jwks": [], + "required_scope": [%s] + }`, JwtIssuer, ToCSVList(ApiScopes)) + + jwt := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + }, + } + + testMutators := []*gatewayv1beta1.Mutator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + { + Handler: &gatewayv1beta1.Handler{ + Name: "idtoken", + }, + }, + } + + noopRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, noop) + jwtRule := GetRuleFor(HeadersApiPath, ApiMethods, testMutators, jwt) + rules := []gatewayv1beta1.Rule{noopRule, jwtRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := istio.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(vs).NotTo(BeNil()) + Expect(len(vs.Spec.Gateways)).To(Equal(1)) + Expect(len(vs.Spec.Hosts)).To(Equal(1)) + Expect(vs.Spec.Hosts[0]).To(Equal(ServiceHost)) + Expect(len(vs.Spec.Http)).To(Equal(2)) + + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(OathkeeperSvc)) + Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(OathkeeperSvcPort)) + Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) + Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) + + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + + Expect(len(vs.Spec.Http[1].Route)).To(Equal(1)) + Expect(vs.Spec.Http[1].Route[0].Destination.Host).To(Equal(ServiceName + "." + ApiNamespace + ".svc.cluster.local")) + Expect(vs.Spec.Http[1].Route[0].Destination.Port.Number).To(Equal(ServicePort)) + Expect(len(vs.Spec.Http[1].Match)).To(Equal(1)) + Expect(vs.Spec.Http[1].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[1].Path)) + + Expect(vs.Spec.Http[1].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(vs.Spec.Http[1].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(vs.Spec.Http[1].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + + Expect(vs.ObjectMeta.Name).To(BeEmpty()) + Expect(vs.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(vs.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(vs.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(ApiUID)) + }) + + It("should return service for two same paths and different methods", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + jwtConfigJSON := fmt.Sprintf(` + { + "trusted_issuers": ["%s"], + "jwks": [], + "required_scope": [%s] + }`, JwtIssuer, ToCSVList(ApiScopes)) + + jwt := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + }, + } + + testMutators := []*gatewayv1beta1.Mutator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + { + Handler: &gatewayv1beta1.Handler{ + Name: "idtoken", + }, + }, + } + getMethod := []string{"GET"} + postMethod := []string{"POST"} + noopRule := GetRuleFor(ApiPath, getMethod, []*gatewayv1beta1.Mutator{}, noop) + jwtRule := GetRuleFor(ApiPath, postMethod, testMutators, jwt) + rules := []gatewayv1beta1.Rule{noopRule, jwtRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := istio.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(vs).NotTo(BeNil()) + Expect(len(vs.Spec.Gateways)).To(Equal(1)) + Expect(len(vs.Spec.Hosts)).To(Equal(1)) + Expect(vs.Spec.Hosts[0]).To(Equal(ServiceHost)) + Expect(len(vs.Spec.Http)).To(Equal(1)) + + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(OathkeeperSvc)) + Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(OathkeeperSvcPort)) + Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) + Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) + + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + + Expect(vs.ObjectMeta.Name).To(BeEmpty()) + Expect(vs.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(vs.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(vs.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(ApiUID)) + }) + + It("should return service for two same paths and one different", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + jwtConfigJSON := fmt.Sprintf(` + { + "trusted_issuers": ["%s"], + "jwks": [], + "required_scope": [%s] + }`, JwtIssuer, ToCSVList(ApiScopes)) + + jwt := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + }, + } + + testMutators := []*gatewayv1beta1.Mutator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + { + Handler: &gatewayv1beta1.Handler{ + Name: "idtoken", + }, + }, + } + getMethod := []string{"GET"} + postMethod := []string{"POST"} + noopGetRule := GetRuleFor(ApiPath, getMethod, []*gatewayv1beta1.Mutator{}, noop) + noopPostRule := GetRuleFor(ApiPath, postMethod, []*gatewayv1beta1.Mutator{}, noop) + jwtRule := GetRuleFor(HeadersApiPath, ApiMethods, testMutators, jwt) + rules := []gatewayv1beta1.Rule{noopGetRule, noopPostRule, jwtRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := istio.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(vs).NotTo(BeNil()) + Expect(len(vs.Spec.Gateways)).To(Equal(1)) + Expect(len(vs.Spec.Hosts)).To(Equal(1)) + Expect(vs.Spec.Hosts[0]).To(Equal(ServiceHost)) + Expect(len(vs.Spec.Http)).To(Equal(2)) + + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(OathkeeperSvc)) + Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(OathkeeperSvcPort)) + Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) + Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) + + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + + Expect(len(vs.Spec.Http[1].Route)).To(Equal(1)) + Expect(vs.Spec.Http[1].Route[0].Destination.Host).To(Equal(ServiceName + "." + ApiNamespace + ".svc.cluster.local")) + Expect(vs.Spec.Http[1].Route[0].Destination.Port.Number).To(Equal(ServicePort)) + Expect(len(vs.Spec.Http[1].Match)).To(Equal(1)) + Expect(vs.Spec.Http[1].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[2].Path)) + + Expect(vs.Spec.Http[1].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(vs.Spec.Http[1].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(vs.Spec.Http[1].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + + Expect(vs.ObjectMeta.Name).To(BeEmpty()) + Expect(vs.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(vs.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(vs.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(ApiUID)) + }) + + It("should return service for jwt & oauth authenticators for given path", func() { + // given + oauthConfigJSON := fmt.Sprintf(`{"required_scope": [%s]}`, ToCSVList(ApiScopes)) + + jwtConfigJSON := fmt.Sprintf(` + { + "trusted_issuers": ["%s"], + "jwks": [], + "required_scope": [%s] + }`, JwtIssuer, ToCSVList(ApiScopes)) + + jwt := &gatewayv1beta1.Authenticator{ + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + } + oauth := &gatewayv1beta1.Authenticator{ + Handler: &gatewayv1beta1.Handler{ + Name: "oauth2_introspection", + Config: &runtime.RawExtension{ + Raw: []byte(oauthConfigJSON), + }, + }, + } + + strategies := []*gatewayv1beta1.Authenticator{jwt, oauth} + + allowRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := istio.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(vs).NotTo(BeNil()) + Expect(len(vs.Spec.Gateways)).To(Equal(1)) + Expect(len(vs.Spec.Hosts)).To(Equal(1)) + Expect(vs.Spec.Hosts[0]).To(Equal(ServiceHost)) + Expect(len(vs.Spec.Http)).To(Equal(1)) + + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(ServiceName + "." + ApiNamespace + ".svc.cluster.local")) + Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(ServicePort)) + + Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) + Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) + + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + + Expect(vs.ObjectMeta.Name).To(BeEmpty()) + Expect(vs.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(vs.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(vs.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(ApiUID)) + }) + }) +}) diff --git a/internal/processing/access_rule_helpers.go b/internal/processing/ory/access_rule_processor.go similarity index 52% rename from internal/processing/access_rule_helpers.go rename to internal/processing/ory/access_rule_processor.go index a958a2ed5..286ab6714 100644 --- a/internal/processing/access_rule_helpers.go +++ b/internal/processing/ory/access_rule_processor.go @@ -1,31 +1,55 @@ -package processing +package ory import ( "fmt" - - "github.com/kyma-incubator/api-gateway/internal/helpers" - gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" "github.com/kyma-incubator/api-gateway/internal/builders" + "github.com/kyma-incubator/api-gateway/internal/helpers" + "github.com/kyma-incubator/api-gateway/internal/processing" rulev1alpha1 "github.com/ory/oathkeeper-maester/api/v1alpha1" ) -func modifyAccessRule(existing, required *rulev1alpha1.Rule) { - existing.Spec = required.Spec +// NewAccessRuleProcessor returns a AccessRuleProcessor with the desired state handling specific for the Ory handler. +func NewAccessRuleProcessor(config processing.ReconciliationConfig) processing.AccessRuleProcessor { + return processing.AccessRuleProcessor{ + Creator: accessRuleCreator{ + additionalLabels: config.AdditionalLabels, + defaultDomainName: config.DefaultDomainName, + }, + } +} + +type accessRuleCreator struct { + additionalLabels map[string]string + defaultDomainName string +} + +// Create returns a map of rules using the configuration of the APIRule. The key of the map is a unique combination of +// the match URL and methods of the rule. +func (r accessRuleCreator) Create(api *gatewayv1beta1.APIRule) map[string]*rulev1alpha1.Rule { + pathDuplicates := processing.HasPathDuplicates(api.Spec.Rules) + accessRules := make(map[string]*rulev1alpha1.Rule) + for _, rule := range api.Spec.Rules { + if processing.IsSecured(rule) { + ar := generateAccessRule(api, rule, rule.AccessStrategies, r.additionalLabels, r.defaultDomainName) + accessRules[processing.SetAccessRuleKey(pathDuplicates, *ar)] = ar + } + } + return accessRules } func generateAccessRule(api *gatewayv1beta1.APIRule, rule gatewayv1beta1.Rule, accessStrategies []*gatewayv1beta1.Authenticator, additionalLabels map[string]string, defaultDomainName string) *rulev1alpha1.Rule { namePrefix := fmt.Sprintf("%s-", api.ObjectMeta.Name) namespace := api.ObjectMeta.Namespace - ownerRef := generateOwnerRef(api) + ownerRef := processing.GenerateOwnerRef(api) arBuilder := builders.AccessRule(). GenerateName(namePrefix). Namespace(namespace). Owner(builders.OwnerReference().From(&ownerRef)). Spec(builders.AccessRuleSpec().From(generateAccessRuleSpec(api, rule, accessStrategies, defaultDomainName))). - Label(OwnerLabel, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)). - Label(OwnerLabelv1alpha1, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)) + Label(processing.OwnerLabel, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)). + Label(processing.OwnerLabelv1alpha1, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)) for k, v := range additionalLabels { arBuilder.Label(k, v) @@ -46,12 +70,12 @@ func generateAccessRuleSpec(api *gatewayv1beta1.APIRule, rule gatewayv1beta1.Rul serviceNamespace := helpers.FindServiceNamespace(api, &rule) - // Use rule level service if it exists if rule.Service != nil { return accessRuleSpec.Upstream(builders.Upstream(). URL(fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", *rule.Service.Name, *serviceNamespace, int(*rule.Service.Port)))).Get() + } else { + return accessRuleSpec.Upstream(builders.Upstream(). + URL(fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", *api.Spec.Service.Name, *serviceNamespace, int(*api.Spec.Service.Port)))).Get() } - // Otherwise use service defined on APIRule spec level - return accessRuleSpec.Upstream(builders.Upstream(). - URL(fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", *api.Spec.Service.Name, *serviceNamespace, int(*api.Spec.Service.Port)))).Get() + } diff --git a/internal/processing/ory/access_rule_processor_test.go b/internal/processing/ory/access_rule_processor_test.go new file mode 100644 index 000000000..5ed6bc5ec --- /dev/null +++ b/internal/processing/ory/access_rule_processor_test.go @@ -0,0 +1,627 @@ +package ory_test + +import ( + "context" + "fmt" + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/processing" + . "github.com/kyma-incubator/api-gateway/internal/processing/internal/test" + "github.com/kyma-incubator/api-gateway/internal/processing/ory" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + "github.com/onsi/gomega/types" + rulev1alpha1 "github.com/ory/oathkeeper-maester/api/v1alpha1" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "strconv" +) + +var idFn = func(index int, element interface{}) string { + return strconv.Itoa(index) +} + +var byteToString = func(raw []byte) string { return string(raw) } + +var _ = Describe("Access Rule Processor", func() { + When("handler is allow", func() { + + It("should not create access rules", func() { + // given + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "allow", + }, + }, + } + + allowRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + + overrideServiceName := "testName" + overrideServiceNamespace := "testName-namespace" + overrideServicePort := uint32(8080) + + apiRule.Spec.Service = &gatewayv1beta1.Service{ + Name: &overrideServiceName, + Namespace: &overrideServiceNamespace, + Port: &overrideServicePort, + } + + client := GetEmptyFakeClient() + processor := ory.NewAccessRuleProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(BeEmpty()) + }) + + }) + + When("handler is noop", func() { + + It("should override rule with meta data", func() { + // given + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + allowRule := GetRuleWithServiceFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies, nil) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := ory.NewAccessRuleProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + accessRule := result[0].Obj.(*rulev1alpha1.Rule) + + Expect(accessRule.ObjectMeta.Name).To(BeEmpty()) + Expect(accessRule.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(accessRule.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(accessRule.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(accessRule.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(accessRule.ObjectMeta.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(accessRule.ObjectMeta.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(accessRule.ObjectMeta.OwnerReferences[0].UID).To(Equal(ApiUID)) + }) + + It("should override rule upstream with rule level service", func() { + // given + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + overrideServiceName := "testName" + overrideServicePort := uint32(8080) + + service := &gatewayv1beta1.Service{ + Name: &overrideServiceName, + Port: &overrideServicePort, + } + + allowRule := GetRuleWithServiceFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies, service) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := ory.NewAccessRuleProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + accessRule := result[0].Obj.(*rulev1alpha1.Rule) + expectedRuleUpstreamURL := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", overrideServiceName, ApiNamespace, overrideServicePort) + Expect(accessRule.Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL)) + }) + + It("should override rule upstream with rule level service for specified namespace", func() { + // given + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + overrideServiceName := "testName" + overrideServiceNamespace := "testName-namespace" + overrideServicePort := uint32(8080) + + service := &gatewayv1beta1.Service{ + Name: &overrideServiceName, + Namespace: &overrideServiceNamespace, + Port: &overrideServicePort, + } + + allowRule := GetRuleWithServiceFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies, service) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := ory.NewAccessRuleProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + accessRule := result[0].Obj.(*rulev1alpha1.Rule) + expectedRuleUpstreamURL := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", overrideServiceName, overrideServiceNamespace, overrideServicePort) + Expect(accessRule.Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL)) + }) + + It("should return rule with default domain name when the hostname does not contain domain name", func() { + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + allowRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + apiRule.Spec.Host = &ServiceHostWithNoDomain + client := GetEmptyFakeClient() + processor := ory.NewAccessRuleProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + expectedRuleMatchURL := fmt.Sprintf("://%s<%s>", ServiceHost, ApiPath) + + accessRule := result[0].Obj.(*rulev1alpha1.Rule) + Expect(accessRule.Spec.Match.URL).To(Equal(expectedRuleMatchURL)) + }) + + Context("when existing rule has owner v1alpha1 owner label", func() { + It("should get and update match methods of rule", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + noopRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, noop) + rules := []gatewayv1beta1.Rule{noopRule} + + apiRule := GetAPIRuleFor(rules) + + rule := rulev1alpha1.Rule{ + + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + processing.OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), + }, + }, + Spec: rulev1alpha1.RuleSpec{ + Match: &rulev1alpha1.Match{ + URL: fmt.Sprintf("://%s<%s>", ServiceHost, ApiPath), + Methods: []string{"DELETE"}, + }, + }, + } + + vs := networkingv1beta1.VirtualService{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + processing.OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), + }, + }, + } + + scheme := runtime.NewScheme() + err := rulev1alpha1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + err = networkingv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + err = gatewayv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&rule, &vs).Build() + processor := ory.NewAccessRuleProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Action.String()).To(Equal("update")) + + accessRule := result[0].Obj.(*rulev1alpha1.Rule) + Expect(accessRule.Spec.Match.Methods).To(Equal([]string{"GET"})) + }) + }) + + }) + + When("multiple handler", func() { + + It("should return two rules for given paths", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + jwtConfigJSON := fmt.Sprintf(` + { + "trusted_issuers": ["%s"], + "jwks": [], + "required_scope": [%s] + }`, JwtIssuer, ToCSVList(ApiScopes)) + + jwt := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + }, + } + + testMutators := []*gatewayv1beta1.Mutator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + { + Handler: &gatewayv1beta1.Handler{ + Name: "idtoken", + }, + }, + } + + noopRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, noop) + jwtRule := GetRuleFor(HeadersApiPath, ApiMethods, testMutators, jwt) + rules := []gatewayv1beta1.Rule{noopRule, jwtRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := ory.NewAccessRuleProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(2)) + + expectedNoopRuleMatchURL := fmt.Sprintf("://%s<%s>", ServiceHost, ApiPath) + expectedJwtRuleMatchURL := fmt.Sprintf("://%s<%s>", ServiceHost, HeadersApiPath) + expectedRuleUpstreamURL := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", ServiceName, ApiNamespace, ServicePort) + + noopMatcher := buildNoopMatcher(ApiMethods, expectedNoopRuleMatchURL, expectedRuleUpstreamURL, "allow") + jwtMatcher := buildJwtMatcher(ApiMethods, expectedJwtRuleMatchURL, expectedRuleUpstreamURL, jwtConfigJSON) + + Expect(result).To(ContainElements(noopMatcher, jwtMatcher)) + }) + + It("should return two rules for two same paths and different methods", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + jwtConfigJSON := fmt.Sprintf(` + { + "trusted_issuers": ["%s"], + "jwks": [], + "required_scope": [%s] + }`, JwtIssuer, ToCSVList(ApiScopes)) + + jwt := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + }, + } + + testMutators := []*gatewayv1beta1.Mutator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + { + Handler: &gatewayv1beta1.Handler{ + Name: "idtoken", + }, + }, + } + getMethod := []string{"GET"} + postMethod := []string{"POST"} + noopRule := GetRuleFor(ApiPath, getMethod, []*gatewayv1beta1.Mutator{}, noop) + jwtRule := GetRuleFor(ApiPath, postMethod, testMutators, jwt) + rules := []gatewayv1beta1.Rule{noopRule, jwtRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := ory.NewAccessRuleProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(2)) + + expectedNoopRuleMatchURL := fmt.Sprintf("://%s<%s>", ServiceHost, ApiPath) + expectedJwtRuleMatchURL := fmt.Sprintf("://%s<%s>", ServiceHost, ApiPath) + expectedRuleUpstreamURL := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", ServiceName, ApiNamespace, ServicePort) + + noopMatcher := buildNoopMatcher(getMethod, expectedNoopRuleMatchURL, expectedRuleUpstreamURL, "allow") + jwtMatcher := buildJwtMatcher(postMethod, expectedJwtRuleMatchURL, expectedRuleUpstreamURL, jwtConfigJSON) + + Expect(result).To(ContainElements(noopMatcher, jwtMatcher)) + }) + + It("should return two rules for two same paths and one different", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + jwtConfigJSON := fmt.Sprintf(` + { + "trusted_issuers": ["%s"], + "jwks": [], + "required_scope": [%s] + }`, JwtIssuer, ToCSVList(ApiScopes)) + + jwt := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + }, + } + + testMutators := []*gatewayv1beta1.Mutator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + { + Handler: &gatewayv1beta1.Handler{ + Name: "idtoken", + }, + }, + } + getMethod := []string{"GET"} + postMethod := []string{"POST"} + noopGetRule := GetRuleFor(ApiPath, getMethod, []*gatewayv1beta1.Mutator{}, noop) + noopPostRule := GetRuleFor(ApiPath, postMethod, []*gatewayv1beta1.Mutator{}, noop) + jwtRule := GetRuleFor(HeadersApiPath, ApiMethods, testMutators, jwt) + rules := []gatewayv1beta1.Rule{noopGetRule, noopPostRule, jwtRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := ory.NewAccessRuleProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(3)) + + expectedNoopRuleMatchURL := fmt.Sprintf("://%s<%s>", ServiceHost, ApiPath) + expectedJwtRuleMatchURL := fmt.Sprintf("://%s<%s>", ServiceHost, HeadersApiPath) + expectedRuleUpstreamURL := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", ServiceName, ApiNamespace, ServicePort) + + noopGetMatcher := buildNoopMatcher(getMethod, expectedNoopRuleMatchURL, expectedRuleUpstreamURL, "allow") + noopPostMatcher := buildNoopMatcher(postMethod, expectedNoopRuleMatchURL, expectedRuleUpstreamURL, "allow") + jwtMatcher := buildJwtMatcher(ApiMethods, expectedJwtRuleMatchURL, expectedRuleUpstreamURL, jwtConfigJSON) + + Expect(result).To(ContainElements(noopGetMatcher, noopPostMatcher, jwtMatcher)) + }) + + It("should return rule for jwt & oauth authenticators for given path", func() { + // given + oauthConfigJSON := fmt.Sprintf(`{"required_scope": [%s]}`, ToCSVList(ApiScopes)) + + jwtConfigJSON := fmt.Sprintf(` + { + "trusted_issuers": ["%s"], + "jwks": [], + "required_scope": [%s] + }`, JwtIssuer, ToCSVList(ApiScopes)) + + jwt := &gatewayv1beta1.Authenticator{ + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + } + oauth := &gatewayv1beta1.Authenticator{ + Handler: &gatewayv1beta1.Handler{ + Name: "oauth2_introspection", + Config: &runtime.RawExtension{ + Raw: []byte(oauthConfigJSON), + }, + }, + } + + strategies := []*gatewayv1beta1.Authenticator{jwt, oauth} + + allowRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := ory.NewAccessRuleProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + rule := result[0].Obj.(*rulev1alpha1.Rule) + + Expect(len(rule.Spec.Authenticators)).To(Equal(2)) + + Expect(rule.Spec.Authorizer.Name).To(Equal("allow")) + Expect(rule.Spec.Authorizer.Config).To(BeNil()) + + Expect(rule.Spec.Authenticators[0].Handler.Name).To(Equal("jwt")) + Expect(rule.Spec.Authenticators[0].Handler.Config).NotTo(BeNil()) + Expect(string(rule.Spec.Authenticators[0].Handler.Config.Raw)).To(Equal(jwtConfigJSON)) + + Expect(rule.Spec.Authenticators[1].Handler.Name).To(Equal("oauth2_introspection")) + Expect(rule.Spec.Authenticators[1].Handler.Config).NotTo(BeNil()) + Expect(string(rule.Spec.Authenticators[1].Handler.Config.Raw)).To(Equal(oauthConfigJSON)) + + expectedRuleMatchURL := fmt.Sprintf("://%s<%s>", ServiceHost, ApiPath) + expectedRuleUpstreamURL := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", ServiceName, ApiNamespace, ServicePort) + + Expect(len(rule.Spec.Match.Methods)).To(Equal(len(ApiMethods))) + Expect(rule.Spec.Match.Methods).To(Equal(ApiMethods)) + Expect(rule.Spec.Match.URL).To(Equal(expectedRuleMatchURL)) + + Expect(rule.Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL)) + }) + }) +}) + +func buildNoopMatcher(matchMethods []string, matchUrl string, upstreamUrl string, authorizerHandler string) types.GomegaMatcher { + + return PointTo(MatchFields(IgnoreExtras, Fields{ + "Obj": PointTo(MatchFields(IgnoreExtras, Fields{ + "Spec": MatchFields(IgnoreExtras, Fields{ + "Match": PointTo(MatchFields(IgnoreExtras, Fields{ + "Methods": Equal(matchMethods), + "URL": Equal(matchUrl), + })), + "Upstream": PointTo(MatchFields(IgnoreExtras, Fields{ + "URL": Equal(upstreamUrl), + })), + "Authorizer": PointTo(MatchFields(IgnoreExtras, Fields{ + "Handler": PointTo(MatchFields(IgnoreExtras, Fields{ + "Name": Equal(authorizerHandler), + "Config": BeNil(), + })), + })), + "Authenticators": MatchElementsWithIndex(idFn, IgnoreExtras, Elements{ + "0": PointTo(MatchFields(IgnoreExtras, Fields{ + "Handler": PointTo(MatchFields(IgnoreExtras, Fields{ + "Name": Equal("noop"), + "Config": BeNil(), + })), + })), + }), + }), + })), + })) +} + +func buildJwtMatcher(matchMethods []string, matchUrl string, upstreamUrl string, jwtConfigJson string) types.GomegaMatcher { + return PointTo(MatchFields(IgnoreExtras, Fields{ + "Obj": PointTo(MatchFields(IgnoreExtras, Fields{ + "Spec": MatchFields(IgnoreExtras, Fields{ + "Match": PointTo(MatchFields(IgnoreExtras, Fields{ + "Methods": Equal(matchMethods), + "URL": Equal(matchUrl), + })), + "Upstream": PointTo(MatchFields(IgnoreExtras, Fields{ + "URL": Equal(upstreamUrl), + })), + "Authorizer": PointTo(MatchFields(IgnoreExtras, Fields{ + "Handler": PointTo(MatchFields(IgnoreExtras, Fields{ + "Name": Equal("allow"), + "Config": BeNil(), + })), + })), + "Authenticators": MatchElementsWithIndex(idFn, IgnoreExtras, Elements{ + "0": PointTo(MatchFields(IgnoreExtras, Fields{ + "Handler": PointTo(MatchFields(IgnoreExtras, Fields{ + "Name": Equal("jwt"), + "Config": PointTo(MatchFields(IgnoreExtras, Fields{ + "Raw": WithTransform(byteToString, Equal(jwtConfigJson)), + })), + })), + })), + }), + "Mutators": MatchElementsWithIndex(idFn, IgnoreExtras, Elements{ + "0": PointTo(MatchFields(IgnoreExtras, Fields{ + "Handler": PointTo(MatchFields(IgnoreExtras, Fields{ + "Name": Equal("noop"), + })), + })), + "1": PointTo(MatchFields(IgnoreExtras, Fields{ + "Handler": PointTo(MatchFields(IgnoreExtras, Fields{ + "Name": Equal("idtoken"), + })), + })), + }), + }), + })), + })) +} diff --git a/internal/processing/ory/jwt_validator.go b/internal/processing/ory/jwt_validator.go new file mode 100644 index 000000000..41f00e940 --- /dev/null +++ b/internal/processing/ory/jwt_validator.go @@ -0,0 +1,59 @@ +package ory + +import ( + "encoding/json" + "fmt" + + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/types/ory" + "github.com/kyma-incubator/api-gateway/internal/validation" +) + +type jwtValidator struct{} + +func (o *jwtValidator) Validate(attributePath string, handler *gatewayv1beta1.Handler) []validation.Failure { + var problems []validation.Failure + + var template ory.JWTAccStrConfig + + if !validation.ConfigNotEmpty(handler.Config) { + problems = append(problems, validation.Failure{AttributePath: attributePath + ".config", Message: "supplied config cannot be empty"}) + return problems + } + err := json.Unmarshal(handler.Config.Raw, &template) + if err != nil { + problems = append(problems, validation.Failure{AttributePath: attributePath + ".config", Message: "Can't read json: " + err.Error()}) + return problems + } + if len(template.TrustedIssuers) > 0 { + for i := 0; i < len(template.TrustedIssuers); i++ { + invalid, err := validation.IsInvalidURL(template.TrustedIssuers[i]) + if invalid { + attrPath := fmt.Sprintf("%s[%d]", attributePath+".config.trusted_issuers", i) + problems = append(problems, validation.Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is empty or not a valid url err=%s", err)}) + } + unsecured, err := validation.IsUnsecuredURL(template.TrustedIssuers[i]) + if unsecured { + attrPath := fmt.Sprintf("%s[%d]", attributePath+".config.trusted_issuers", i) + problems = append(problems, validation.Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is not a secured url err=%s", err)}) + } + } + } + + if len(template.JWKSUrls) > 0 { + for i := 0; i < len(template.JWKSUrls); i++ { + invalid, err := validation.IsInvalidURL(template.JWKSUrls[i]) + if invalid { + attrPath := fmt.Sprintf("%s[%d]", attributePath+".config.jwks_urls", i) + problems = append(problems, validation.Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is empty or not a valid url err=%s", err)}) + } + unsecured, err := validation.IsUnsecuredURL(template.JWKSUrls[i]) + if unsecured { + attrPath := fmt.Sprintf("%s[%d]", attributePath+".config.jwks_urls", i) + problems = append(problems, validation.Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is not a secured url err=%s", err)}) + } + } + } + + return problems +} diff --git a/internal/processing/ory/jwt_validator_test.go b/internal/processing/ory/jwt_validator_test.go new file mode 100644 index 000000000..27cd96307 --- /dev/null +++ b/internal/processing/ory/jwt_validator_test.go @@ -0,0 +1,134 @@ +package ory + +import ( + "encoding/json" + + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/types/ory" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ = Describe("JWT Validator", func() { + + It("Should fail with empty config", func() { + //given + handler := &gatewayv1beta1.Handler{Name: "jwt", Config: emptyConfig()} + + //when + problems := (&jwtValidator{}).Validate("some.attribute", handler) + + //then + Expect(problems).To(HaveLen(1)) + Expect(problems[0].AttributePath).To(Equal("some.attribute.config")) + Expect(problems[0].Message).To(Equal("supplied config cannot be empty")) + }) + + It("Should fail for config with invalid trustedIssuers and JWKSUrls", func() { + //given + handler := &gatewayv1beta1.Handler{Name: "jwt", Config: simpleJWTConfig("a t g o")} + + //when + problems := (&jwtValidator{}).Validate("some.attribute", handler) + + //then + Expect(problems).To(HaveLen(2)) + Expect(problems[0].AttributePath).To(Equal("some.attribute.config.trusted_issuers[0]")) + Expect(problems[0].Message).To(ContainSubstring("value is empty or not a valid url")) + Expect(problems[1].AttributePath).To(Equal("some.attribute.config.jwks_urls[0]")) + Expect(problems[1].Message).To(ContainSubstring("value is empty or not a valid url")) + }) + + It("Should fail for config with plain HTTP JWKSUrls and trustedIssuers", func() { + //given + handler := &gatewayv1beta1.Handler{Name: "jwt", Config: testURLJWTConfig("http://issuer.test/.well-known/jwks.json", "http://issuer.test/")} + + //when + problems := (&jwtValidator{}).Validate("some.attribute", handler) + + //then + Expect(problems).To(HaveLen(2)) + Expect(problems[0].AttributePath).To(Equal("some.attribute.config.trusted_issuers[0]")) + Expect(problems[0].Message).To(ContainSubstring("value is not a secured url")) + Expect(problems[1].AttributePath).To(Equal("some.attribute.config.jwks_urls[0]")) + Expect(problems[1].Message).To(ContainSubstring("value is not a secured url")) + }) + + It("Should succeed for config with file JWKSUrls and HTTPS trustedIssuers", func() { + //given + handler := &gatewayv1beta1.Handler{Name: "jwt", Config: testURLJWTConfig("file://.well-known/jwks.json", "https://issuer.test/")} + + //when + problems := (&jwtValidator{}).Validate("some.attribute", handler) + + //then + Expect(problems).To(HaveLen(0)) + }) + + It("Should succeed for config with HTTPS JWKSUrls and trustedIssuers", func() { + //given + handler := &gatewayv1beta1.Handler{Name: "jwt", Config: testURLJWTConfig("https://issuer.test/.well-known/jwks.json", "https://issuer.test/")} + + //when + problems := (&jwtValidator{}).Validate("some.attribute", handler) + + //then + Expect(problems).To(HaveLen(0)) + }) + + It("Should fail for invalid JSON", func() { + //given + handler := &gatewayv1beta1.Handler{Name: "jwt", Config: &runtime.RawExtension{Raw: []byte("/abc]")}} + + //when + problems := (&jwtValidator{}).Validate("some.attribute", handler) + + //then + Expect(problems).To(HaveLen(1)) + Expect(problems[0].AttributePath).To(Equal("some.attribute.config")) + Expect(problems[0].Message).To(Equal("Can't read json: invalid character '/' looking for beginning of value")) + }) + + It("Should succeed with valid config", func() { + //given + handler := &gatewayv1beta1.Handler{Name: "jwt", Config: simpleJWTConfig()} + + //when + problems := (&jwtValidator{}).Validate("some.attribute", handler) + + //then + Expect(problems).To(HaveLen(0)) + }) +}) + +func emptyConfig() *runtime.RawExtension { + return getRawConfig( + &ory.JWTAccStrConfig{}) +} + +func simpleJWTConfig(trustedIssuers ...string) *runtime.RawExtension { + return getRawConfig( + &ory.JWTAccStrConfig{ + JWKSUrls: trustedIssuers, + TrustedIssuers: trustedIssuers, + RequiredScopes: []string{"atgo"}, + }) +} + +func testURLJWTConfig(JWKSUrls string, trustedIssuers string) *runtime.RawExtension { + return getRawConfig( + &ory.JWTAccStrConfig{ + JWKSUrls: []string{JWKSUrls}, + TrustedIssuers: []string{trustedIssuers}, + RequiredScopes: []string{"atgo"}, + }) +} + +func getRawConfig(config *ory.JWTAccStrConfig) *runtime.RawExtension { + bytes, err := json.Marshal(config) + Expect(err).To(BeNil()) + return &runtime.RawExtension{ + Raw: bytes, + } +} diff --git a/internal/processing/ory/ory_suite_test.go b/internal/processing/ory/ory_suite_test.go new file mode 100644 index 000000000..b70854e04 --- /dev/null +++ b/internal/processing/ory/ory_suite_test.go @@ -0,0 +1,14 @@ +package ory + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/envtest/printer" + "testing" +) + +func TestOry(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecsWithDefaultAndCustomReporters(t, "Ory Suite", + []Reporter{printer.NewProwReporter("api-gateway-ory-testsuite")}) +} diff --git a/internal/processing/ory/reconciliation.go b/internal/processing/ory/reconciliation.go new file mode 100644 index 000000000..ebebc61ba --- /dev/null +++ b/internal/processing/ory/reconciliation.go @@ -0,0 +1,46 @@ +package ory + +import ( + "context" + + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/processing" + "github.com/kyma-incubator/api-gateway/internal/validation" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Reconciliation struct { + processors []processing.ReconciliationProcessor + config processing.ReconciliationConfig +} + +func NewOryReconciliation(config processing.ReconciliationConfig) Reconciliation { + vsProcessor := NewVirtualServiceProcessor(config) + acProcessor := NewAccessRuleProcessor(config) + + return Reconciliation{ + processors: []processing.ReconciliationProcessor{vsProcessor, acProcessor}, + config: config, + } +} + +func (r Reconciliation) Validate(ctx context.Context, client client.Client, apiRule *gatewayv1beta1.APIRule) ([]validation.Failure, error) { + var vsList networkingv1beta1.VirtualServiceList + if err := client.List(ctx, &vsList); err != nil { + return make([]validation.Failure, 0), err + } + + validator := validation.APIRule{ + JwtValidator: &jwtValidator{}, + ServiceBlockList: r.config.ServiceBlockList, + DomainAllowList: r.config.DomainAllowList, + HostBlockList: r.config.HostBlockList, + DefaultDomainName: r.config.DefaultDomainName, + } + return validator.Validate(apiRule, vsList), nil +} + +func (r Reconciliation) GetProcessors() []processing.ReconciliationProcessor { + return r.processors +} diff --git a/internal/processing/ory/virtual_service_processor.go b/internal/processing/ory/virtual_service_processor.go new file mode 100644 index 000000000..fe8a7a603 --- /dev/null +++ b/internal/processing/ory/virtual_service_processor.go @@ -0,0 +1,99 @@ +package ory + +import ( + "fmt" + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/builders" + "github.com/kyma-incubator/api-gateway/internal/helpers" + "github.com/kyma-incubator/api-gateway/internal/processing" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" +) + +// NewVirtualServiceProcessor returns a VirtualServiceProcessor with the desired state handling specific for the Ory handler. +func NewVirtualServiceProcessor(config processing.ReconciliationConfig) processing.VirtualServiceProcessor { + return processing.VirtualServiceProcessor{ + Creator: virtualServiceCreator{ + oathkeeperSvc: config.OathkeeperSvc, + oathkeeperSvcPort: config.OathkeeperSvcPort, + corsConfig: config.CorsConfig, + additionalLabels: config.AdditionalLabels, + defaultDomainName: config.DefaultDomainName, + }, + } +} + +type virtualServiceCreator struct { + oathkeeperSvc string + oathkeeperSvcPort uint32 + corsConfig *processing.CorsConfig + defaultDomainName string + additionalLabels map[string]string +} + +// Create returns the Virtual Service using the configuration of the APIRule. +func (r virtualServiceCreator) Create(api *gatewayv1beta1.APIRule) *networkingv1beta1.VirtualService { + virtualServiceNamePrefix := fmt.Sprintf("%s-", api.ObjectMeta.Name) + ownerRef := processing.GenerateOwnerRef(api) + + vsSpecBuilder := builders.VirtualServiceSpec() + vsSpecBuilder.Host(helpers.GetHostWithDomain(*api.Spec.Host, r.defaultDomainName)) + vsSpecBuilder.Gateway(*api.Spec.Gateway) + filteredRules := filterDuplicatePaths(api.Spec.Rules) + + for _, rule := range filteredRules { + httpRouteBuilder := builders.HTTPRoute() + host, port := r.oathkeeperSvc, r.oathkeeperSvcPort + serviceNamespace := helpers.FindServiceNamespace(api, &rule) + + if !processing.IsSecured(rule) { + // Use rule level service if it exists + if rule.Service != nil { + host = fmt.Sprintf("%s.%s.svc.cluster.local", *rule.Service.Name, *serviceNamespace) + port = *rule.Service.Port + } else { + // Otherwise use service defined on APIRule spec level + host = fmt.Sprintf("%s.%s.svc.cluster.local", *api.Spec.Service.Name, *serviceNamespace) + port = *api.Spec.Service.Port + } + } + + httpRouteBuilder.Route(builders.RouteDestination().Host(host).Port(port)) + httpRouteBuilder.Match(builders.MatchRequest().Uri().Regex(rule.Path)) + httpRouteBuilder.CorsPolicy(builders.CorsPolicy(). + AllowOrigins(r.corsConfig.AllowOrigins...). + AllowMethods(r.corsConfig.AllowMethods...). + AllowHeaders(r.corsConfig.AllowHeaders...)) + httpRouteBuilder.Headers(builders.Headers(). + SetHostHeader(helpers.GetHostWithDomain(*api.Spec.Host, r.defaultDomainName))) + vsSpecBuilder.HTTP(httpRouteBuilder) + + } + + vsBuilder := builders.VirtualService(). + GenerateName(virtualServiceNamePrefix). + Namespace(api.ObjectMeta.Namespace). + Owner(builders.OwnerReference().From(&ownerRef)). + Label(processing.OwnerLabel, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)). + Label(processing.OwnerLabelv1alpha1, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)) + + for k, v := range r.additionalLabels { + vsBuilder.Label(k, v) + } + + vsBuilder.Spec(vsSpecBuilder) + + return vsBuilder.Get() +} + +func filterDuplicatePaths(rules []gatewayv1beta1.Rule) []gatewayv1beta1.Rule { + duplicates := make(map[string]bool) + var filteredRules []gatewayv1beta1.Rule + for _, rule := range rules { + if _, exists := duplicates[rule.Path]; !exists { + duplicates[rule.Path] = true + filteredRules = append(filteredRules, rule) + } + } + + return filteredRules +} diff --git a/internal/processing/ory/virtual_service_processor_test.go b/internal/processing/ory/virtual_service_processor_test.go new file mode 100644 index 000000000..ad954e9f6 --- /dev/null +++ b/internal/processing/ory/virtual_service_processor_test.go @@ -0,0 +1,661 @@ +package ory_test + +import ( + "context" + "fmt" + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/processing" + . "github.com/kyma-incubator/api-gateway/internal/processing/internal/test" + "github.com/kyma-incubator/api-gateway/internal/processing/ory" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + rulev1alpha1 "github.com/ory/oathkeeper-maester/api/v1alpha1" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var _ = Describe("Virtual Service Processor", func() { + When("handler is allow", func() { + It("should create for allow authenticator", func() { + // given + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "allow", + }, + }, + } + + allowRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := ory.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Action.String()).To(Equal("create")) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(vs).NotTo(BeNil()) + Expect(len(vs.Spec.Gateways)).To(Equal(1)) + Expect(len(vs.Spec.Hosts)).To(Equal(1)) + Expect(vs.Spec.Hosts[0]).To(Equal(ServiceHost)) + Expect(len(vs.Spec.Http)).To(Equal(1)) + + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(ServiceName + "." + ApiNamespace + ".svc.cluster.local")) + Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(ServicePort)) + + Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) + Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) + + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + + Expect(vs.ObjectMeta.Name).To(BeEmpty()) + Expect(vs.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(vs.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(vs.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(ApiUID)) + }) + + It("should override destination host for specified spec level service namespace", func() { + // given + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "allow", + }, + }, + } + + allowRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + + overrideServiceName := "testName" + overrideServiceNamespace := "testName-namespace" + overrideServicePort := uint32(8080) + + apiRule.Spec.Service = &gatewayv1beta1.Service{ + Name: &overrideServiceName, + Namespace: &overrideServiceNamespace, + Port: &overrideServicePort, + } + client := GetEmptyFakeClient() + processor := ory.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(overrideServiceName + "." + overrideServiceNamespace + ".svc.cluster.local")) + }) + + It("should override destination host with rule level service namespace", func() { + // given + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "allow", + }, + }, + } + + overrideServiceName := "testName" + overrideServiceNamespace := "testName-namespace" + overrideServicePort := uint32(8080) + + service := &gatewayv1beta1.Service{ + Name: &overrideServiceName, + Namespace: &overrideServiceNamespace, + Port: &overrideServicePort, + } + + allowRule := GetRuleWithServiceFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies, service) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := ory.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + //verify VS has rule level destination host + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(overrideServiceName + "." + overrideServiceNamespace + ".svc.cluster.local")) + + }) + It("should return VS with default domain name when the hostname does not contain domain name", func() { + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "allow", + }, + }, + } + + allowRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + apiRule.Spec.Host = &ServiceHostWithNoDomain + client := GetEmptyFakeClient() + processor := ory.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + //verify VS + Expect(vs).NotTo(BeNil()) + Expect(len(vs.Spec.Hosts)).To(Equal(1)) + Expect(vs.Spec.Hosts[0]).To(Equal(ServiceHost)) + + }) + }) + + When("handler is noop", func() { + It("should not override Oathkeeper service destination host with spec level service", func() { + // given + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + overrideServiceName := "testName" + overrideServicePort := uint32(8080) + + service := &gatewayv1beta1.Service{ + Name: &overrideServiceName, + Port: &overrideServicePort, + } + + allowRule := GetRuleWithServiceFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies, service) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := ory.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(OathkeeperSvc)) + }) + When("existing virtual service has owner v1alpha1 owner label", func() { + It("should get and update", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + noopRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, noop) + rules := []gatewayv1beta1.Rule{noopRule} + + apiRule := GetAPIRuleFor(rules) + + rule := rulev1alpha1.Rule{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + processing.OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), + }, + }, + Spec: rulev1alpha1.RuleSpec{ + Match: &rulev1alpha1.Match{ + URL: "some url", + }, + }, + } + + vs := networkingv1beta1.VirtualService{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + processing.OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), + }, + }, + } + + scheme := runtime.NewScheme() + err := rulev1alpha1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + err = networkingv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + err = gatewayv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&rule, &vs).Build() + processor := ory.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Action.String()).To(Equal("update")) + + resultVs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(resultVs).NotTo(BeNil()) + Expect(resultVs).NotTo(BeNil()) + Expect(len(resultVs.Spec.Gateways)).To(Equal(1)) + Expect(len(resultVs.Spec.Hosts)).To(Equal(1)) + Expect(resultVs.Spec.Hosts[0]).To(Equal(ServiceHost)) + Expect(len(resultVs.Spec.Http)).To(Equal(1)) + + Expect(len(resultVs.Spec.Http[0].Route)).To(Equal(1)) + Expect(resultVs.Spec.Http[0].Route[0].Destination.Host).To(Equal(OathkeeperSvc)) + Expect(resultVs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(OathkeeperSvcPort)) + + Expect(len(resultVs.Spec.Http[0].Match)).To(Equal(1)) + Expect(resultVs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) + + Expect(resultVs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(resultVs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(resultVs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + }) + }) + }) + + When("multiple handler", func() { + It("should return service for given paths", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + jwtConfigJSON := fmt.Sprintf(` + { + "trusted_issuers": ["%s"], + "jwks": [], + "required_scope": [%s] + }`, JwtIssuer, ToCSVList(ApiScopes)) + + jwt := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + }, + } + + testMutators := []*gatewayv1beta1.Mutator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + { + Handler: &gatewayv1beta1.Handler{ + Name: "idtoken", + }, + }, + } + + noopRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, noop) + jwtRule := GetRuleFor(HeadersApiPath, ApiMethods, testMutators, jwt) + rules := []gatewayv1beta1.Rule{noopRule, jwtRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := ory.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(vs).NotTo(BeNil()) + Expect(len(vs.Spec.Gateways)).To(Equal(1)) + Expect(len(vs.Spec.Hosts)).To(Equal(1)) + Expect(vs.Spec.Hosts[0]).To(Equal(ServiceHost)) + Expect(len(vs.Spec.Http)).To(Equal(2)) + + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(OathkeeperSvc)) + Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(OathkeeperSvcPort)) + Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) + Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) + + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + + Expect(len(vs.Spec.Http[1].Route)).To(Equal(1)) + Expect(vs.Spec.Http[1].Route[0].Destination.Host).To(Equal(OathkeeperSvc)) + Expect(vs.Spec.Http[1].Route[0].Destination.Port.Number).To(Equal(OathkeeperSvcPort)) + Expect(len(vs.Spec.Http[1].Match)).To(Equal(1)) + Expect(vs.Spec.Http[1].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[1].Path)) + + Expect(vs.Spec.Http[1].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(vs.Spec.Http[1].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(vs.Spec.Http[1].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + + Expect(vs.ObjectMeta.Name).To(BeEmpty()) + Expect(vs.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(vs.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(vs.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(ApiUID)) + }) + + It("should return service for two same paths and different methods", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + jwtConfigJSON := fmt.Sprintf(` + { + "trusted_issuers": ["%s"], + "jwks": [], + "required_scope": [%s] + }`, JwtIssuer, ToCSVList(ApiScopes)) + + jwt := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + }, + } + + testMutators := []*gatewayv1beta1.Mutator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + { + Handler: &gatewayv1beta1.Handler{ + Name: "idtoken", + }, + }, + } + getMethod := []string{"GET"} + postMethod := []string{"POST"} + noopRule := GetRuleFor(ApiPath, getMethod, []*gatewayv1beta1.Mutator{}, noop) + jwtRule := GetRuleFor(ApiPath, postMethod, testMutators, jwt) + rules := []gatewayv1beta1.Rule{noopRule, jwtRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := ory.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(vs).NotTo(BeNil()) + Expect(len(vs.Spec.Gateways)).To(Equal(1)) + Expect(len(vs.Spec.Hosts)).To(Equal(1)) + Expect(vs.Spec.Hosts[0]).To(Equal(ServiceHost)) + Expect(len(vs.Spec.Http)).To(Equal(1)) + + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(OathkeeperSvc)) + Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(OathkeeperSvcPort)) + Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) + Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) + + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + + Expect(vs.ObjectMeta.Name).To(BeEmpty()) + Expect(vs.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(vs.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(vs.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(ApiUID)) + }) + + It("should return service for two same paths and one different", func() { + // given + noop := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + } + + jwtConfigJSON := fmt.Sprintf(` + { + "trusted_issuers": ["%s"], + "jwks": [], + "required_scope": [%s] + }`, JwtIssuer, ToCSVList(ApiScopes)) + + jwt := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + }, + } + + testMutators := []*gatewayv1beta1.Mutator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "noop", + }, + }, + { + Handler: &gatewayv1beta1.Handler{ + Name: "idtoken", + }, + }, + } + getMethod := []string{"GET"} + postMethod := []string{"POST"} + noopGetRule := GetRuleFor(ApiPath, getMethod, []*gatewayv1beta1.Mutator{}, noop) + noopPostRule := GetRuleFor(ApiPath, postMethod, []*gatewayv1beta1.Mutator{}, noop) + jwtRule := GetRuleFor(HeadersApiPath, ApiMethods, testMutators, jwt) + rules := []gatewayv1beta1.Rule{noopGetRule, noopPostRule, jwtRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := ory.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(vs).NotTo(BeNil()) + Expect(len(vs.Spec.Gateways)).To(Equal(1)) + Expect(len(vs.Spec.Hosts)).To(Equal(1)) + Expect(vs.Spec.Hosts[0]).To(Equal(ServiceHost)) + Expect(len(vs.Spec.Http)).To(Equal(2)) + + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(OathkeeperSvc)) + Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(OathkeeperSvcPort)) + Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) + Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) + + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + + Expect(len(vs.Spec.Http[1].Route)).To(Equal(1)) + Expect(vs.Spec.Http[1].Route[0].Destination.Host).To(Equal(OathkeeperSvc)) + Expect(vs.Spec.Http[1].Route[0].Destination.Port.Number).To(Equal(OathkeeperSvcPort)) + Expect(len(vs.Spec.Http[1].Match)).To(Equal(1)) + Expect(vs.Spec.Http[1].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[2].Path)) + + Expect(vs.Spec.Http[1].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(vs.Spec.Http[1].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(vs.Spec.Http[1].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + + Expect(vs.ObjectMeta.Name).To(BeEmpty()) + Expect(vs.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(vs.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(vs.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(ApiUID)) + }) + + It("should return service for jwt & oauth authenticators for given path", func() { + // given + oauthConfigJSON := fmt.Sprintf(`{"required_scope": [%s]}`, ToCSVList(ApiScopes)) + + jwtConfigJSON := fmt.Sprintf(` + { + "trusted_issuers": ["%s"], + "jwks": [], + "required_scope": [%s] + }`, JwtIssuer, ToCSVList(ApiScopes)) + + jwt := &gatewayv1beta1.Authenticator{ + Handler: &gatewayv1beta1.Handler{ + Name: "jwt", + Config: &runtime.RawExtension{ + Raw: []byte(jwtConfigJSON), + }, + }, + } + oauth := &gatewayv1beta1.Authenticator{ + Handler: &gatewayv1beta1.Handler{ + Name: "oauth2_introspection", + Config: &runtime.RawExtension{ + Raw: []byte(oauthConfigJSON), + }, + }, + } + + strategies := []*gatewayv1beta1.Authenticator{jwt, oauth} + + allowRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + client := GetEmptyFakeClient() + processor := ory.NewVirtualServiceProcessor(GetTestConfig()) + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + + vs := result[0].Obj.(*networkingv1beta1.VirtualService) + + Expect(vs).NotTo(BeNil()) + Expect(len(vs.Spec.Gateways)).To(Equal(1)) + Expect(len(vs.Spec.Hosts)).To(Equal(1)) + Expect(vs.Spec.Hosts[0]).To(Equal(ServiceHost)) + Expect(len(vs.Spec.Http)).To(Equal(1)) + + Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) + Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(OathkeeperSvc)) + Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(OathkeeperSvcPort)) + + Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) + Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) + + Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(TestCors.AllowOrigins)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(TestCors.AllowMethods)) + Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(TestCors.AllowHeaders)) + + Expect(vs.ObjectMeta.Name).To(BeEmpty()) + Expect(vs.ObjectMeta.GenerateName).To(Equal(ApiName + "-")) + Expect(vs.ObjectMeta.Namespace).To(Equal(ApiNamespace)) + Expect(vs.ObjectMeta.Labels[TestLabelKey]).To(Equal(TestLabelValue)) + + Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(ApiAPIVersion)) + Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(ApiKind)) + Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(ApiName)) + Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(ApiUID)) + }) + }) +}) diff --git a/internal/processing/processing.go b/internal/processing/processing.go deleted file mode 100644 index 845c61b74..000000000 --- a/internal/processing/processing.go +++ /dev/null @@ -1,318 +0,0 @@ -package processing - -import ( - "context" - "fmt" - - "istio.io/api/networking/v1beta1" - - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/go-logr/logr" - gatewayv1alpha1 "github.com/kyma-incubator/api-gateway/api/v1alpha1" - gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" - "github.com/kyma-incubator/api-gateway/internal/helpers" - rulev1alpha1 "github.com/ory/oathkeeper-maester/api/v1alpha1" - networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" - istiosecurityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" -) - -var ( - //OwnerLabel . - OwnerLabel = fmt.Sprintf("%s.%s", "apirule", gatewayv1beta1.GroupVersion.String()) - //OwnerLabelv1alpha1 . - OwnerLabelv1alpha1 = fmt.Sprintf("%s.%s", "apirule", gatewayv1alpha1.GroupVersion.String()) -) - -// Factory . -type Factory struct { - client client.Client - Log logr.Logger - oathkeeperSvc string - oathkeeperSvcPort uint32 - corsConfig *CorsConfig - additionalLabels map[string]string - defaultDomainName string -} - -// NewFactory . -func NewFactory(client client.Client, logger logr.Logger, oathkeeperSvc string, oathkeeperSvcPort uint32, corsConfig *CorsConfig, additionalLabels map[string]string, defaultDomainName string) *Factory { - return &Factory{ - client: client, - Log: logger, - oathkeeperSvc: oathkeeperSvc, - oathkeeperSvcPort: oathkeeperSvcPort, - corsConfig: corsConfig, - additionalLabels: additionalLabels, - defaultDomainName: defaultDomainName, - } -} - -// CorsConfig is an internal representation of v1alpha3.CorsPolicy object -type CorsConfig struct { - AllowOrigins []*v1beta1.StringMatch - AllowMethods []string - AllowHeaders []string -} - -// CalculateRequiredState returns required state of all objects related to given api -func (f *Factory) CalculateRequiredState(api *gatewayv1beta1.APIRule, config *helpers.Config) *State { - var res State - hasPathDuplicates := checkPathDuplicates(api.Spec.Rules) - res.accessRules = make(map[string]*rulev1alpha1.Rule) - res.authorizationPolicies = make(map[string]*istiosecurityv1beta1.AuthorizationPolicy) - res.requestAuthentications = make(map[string]*istiosecurityv1beta1.RequestAuthentication) - - for _, rule := range api.Spec.Rules { - if isSecured(rule) { - var ar *rulev1alpha1.Rule - var ap *istiosecurityv1beta1.AuthorizationPolicy - var ra *istiosecurityv1beta1.RequestAuthentication - - switch config.JWTHandler { - case helpers.JWT_HANDLER_ORY: - ar = generateAccessRule(api, rule, rule.AccessStrategies, f.additionalLabels, f.defaultDomainName) - res.accessRules[setAccessRuleKey(hasPathDuplicates, *ar)] = ar - case helpers.JWT_HANDLER_ISTIO: - ap = generateAuthorizationPolicy(api, rule, f.additionalLabels) - ra = generateRequestAuthentication(api, rule, f.additionalLabels) - res.authorizationPolicies[getAuthorizationPolicyKey(hasPathDuplicates, ap)] = ap - res.requestAuthentications[getRequestAuthenticationKey(ra)] = ra - } - } - } - - res.virtualService = f.generateVirtualService(api, config) - - return &res -} - -// State represents desired or actual state of Istio Virtual Services and Oathkeeper Rules -type State struct { - virtualService *networkingv1beta1.VirtualService - accessRules map[string]*rulev1alpha1.Rule - authorizationPolicies map[string]*istiosecurityv1beta1.AuthorizationPolicy - requestAuthentications map[string]*istiosecurityv1beta1.RequestAuthentication -} - -// GetActualState methods gets actual state of Istio Virtual Services and Oathkeeper Rules -func (f *Factory) GetActualState(ctx context.Context, api *gatewayv1beta1.APIRule, config *helpers.Config) (*State, error) { - labels := make(map[string]string) - labels[OwnerLabelv1alpha1] = fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace) - - pathDuplicates := checkPathDuplicates(api.Spec.Rules) - var state State - var vsList networkingv1beta1.VirtualServiceList - - if err := f.client.List(ctx, &vsList, client.MatchingLabels(labels)); err != nil { - return nil, err - } - - if len(vsList.Items) >= 1 { - state.virtualService = vsList.Items[0] - } else { - state.virtualService = nil - } - - var arList rulev1alpha1.RuleList - if err := f.client.List(ctx, &arList, client.MatchingLabels(labels)); err != nil { - return nil, err - } - - state.accessRules = make(map[string]*rulev1alpha1.Rule) - - for i := range arList.Items { - obj := arList.Items[i] - state.accessRules[setAccessRuleKey(pathDuplicates, obj)] = &obj - } - - if config.JWTHandler == helpers.JWT_HANDLER_ISTIO { - var apList istiosecurityv1beta1.AuthorizationPolicyList - if err := f.client.List(ctx, &apList, client.MatchingLabels(labels)); err != nil { - return nil, err - } - - state.authorizationPolicies = make(map[string]*istiosecurityv1beta1.AuthorizationPolicy) - for i := range apList.Items { - obj := apList.Items[i] - state.authorizationPolicies[getAuthorizationPolicyKey(pathDuplicates, obj)] = obj - } - - var raList istiosecurityv1beta1.RequestAuthenticationList - - if err := f.client.List(ctx, &raList, client.MatchingLabels(labels)); err != nil { - return nil, err - } - - state.requestAuthentications = make(map[string]*istiosecurityv1beta1.RequestAuthentication) - for i := range raList.Items { - obj := raList.Items[i] - state.requestAuthentications[getRequestAuthenticationKey(obj)] = obj - } - } - - return &state, nil -} - -// Patch represents diff between desired and actual state -type Patch struct { - virtualService *objToPatch - accessRule map[string]*objToPatch - authorizationPolicy map[string]*objToPatch - requestAuthentication map[string]*objToPatch -} - -type objToPatch struct { - action string - obj client.Object -} - -// CalculateDiff methods compute diff between desired & actual state -func (f *Factory) CalculateDiff(requiredState *State, actualState *State, config *helpers.Config) *Patch { - arPatch := make(map[string]*objToPatch) - accessRulePatch(arPatch, actualState, requiredState) - - apPatch := make(map[string]*objToPatch) - raPatch := make(map[string]*objToPatch) - if config.JWTHandler == helpers.JWT_HANDLER_ISTIO { - authorizationPolicyPatch(apPatch, actualState, requiredState) - requestAuthenticationPatch(raPatch, actualState, requiredState) - } - - vsPatch := &objToPatch{} - if actualState.virtualService != nil { - vsPatch.action = "update" - f.updateVirtualService(actualState.virtualService, requiredState.virtualService) - vsPatch.obj = actualState.virtualService - } else { - vsPatch.action = "create" - vsPatch.obj = requiredState.virtualService - } - - return &Patch{virtualService: vsPatch, accessRule: arPatch, authorizationPolicy: apPatch, requestAuthentication: raPatch} -} - -// ApplyDiff method applies computed diff -func (f *Factory) ApplyDiff(ctx context.Context, patch *Patch, config *helpers.Config) error { - - err := f.applyObjDiff(ctx, patch.virtualService) - if err != nil { - return err - } - - for _, rule := range patch.accessRule { - err := f.applyObjDiff(ctx, rule) - if err != nil { - return err - } - } - - if config.JWTHandler == helpers.JWT_HANDLER_ISTIO { - for _, authorizationPolicy := range patch.authorizationPolicy { - err := f.applyObjDiff(ctx, authorizationPolicy) - if err != nil { - return err - } - } - - for _, requestAuthentication := range patch.requestAuthentication { - err := f.applyObjDiff(ctx, requestAuthentication) - if err != nil { - return err - } - } - } - - return nil -} - -func (f *Factory) applyObjDiff(ctx context.Context, objToPatch *objToPatch) error { - var err error - - switch objToPatch.action { - case "create": - err = f.client.Create(ctx, objToPatch.obj) - case "update": - err = f.client.Update(ctx, objToPatch.obj) - case "delete": - err = f.client.Delete(ctx, objToPatch.obj) - } - - if err != nil { - return err - } - - return nil -} - -func accessRulePatch(arPatch map[string]*objToPatch, actualState, requiredState *State) { - for path, rule := range requiredState.accessRules { - rulePatch := &objToPatch{} - - if actualState.accessRules[path] != nil { - rulePatch.action = "update" - modifyAccessRule(actualState.accessRules[path], rule) - rulePatch.obj = actualState.accessRules[path] - } else { - rulePatch.action = "create" - rulePatch.obj = rule - } - - arPatch[path] = rulePatch - } - - for path, rule := range actualState.accessRules { - if requiredState.accessRules[path] == nil { - objToDelete := &objToPatch{action: "delete", obj: rule} - arPatch[path] = objToDelete - } - } -} - -func authorizationPolicyPatch(apPatch map[string]*objToPatch, actualState, requiredState *State) { - for path, authorizationPolicy := range requiredState.authorizationPolicies { - authorizationPolicyPatch := &objToPatch{} - - if actualState.authorizationPolicies[path] != nil { - authorizationPolicyPatch.action = "update" - modifyAuthorizationPolicy(actualState.authorizationPolicies[path], authorizationPolicy) - authorizationPolicyPatch.obj = actualState.authorizationPolicies[path] - } else { - authorizationPolicyPatch.action = "create" - authorizationPolicyPatch.obj = authorizationPolicy - } - - apPatch[path] = authorizationPolicyPatch - } - - for path, authorizationPolicy := range actualState.authorizationPolicies { - if requiredState.authorizationPolicies[path] == nil { - objToDelete := &objToPatch{action: "delete", obj: authorizationPolicy} - apPatch[path] = objToDelete - } - } -} - -func requestAuthenticationPatch(raPatch map[string]*objToPatch, actualState, requiredState *State) { - for path, requestAuthentication := range requiredState.requestAuthentications { - requestAuthenticationPatch := &objToPatch{} - - if actualState.requestAuthentications[path] != nil { - requestAuthenticationPatch.action = "update" - modifyRequestAuthentication(actualState.requestAuthentications[path], requestAuthentication) - requestAuthenticationPatch.obj = actualState.requestAuthentications[path] - } else { - requestAuthenticationPatch.action = "create" - requestAuthenticationPatch.obj = requestAuthentication - } - - raPatch[path] = requestAuthenticationPatch - } - - for path, requestAuthentication := range actualState.requestAuthentications { - if requiredState.requestAuthentications[path] == nil { - objToDelete := &objToPatch{action: "delete", obj: requestAuthentication} - raPatch[path] = objToDelete - } - } -} diff --git a/internal/processing/processing_suite_test.go b/internal/processing/processing_suite_test.go new file mode 100644 index 000000000..7a8d75ecf --- /dev/null +++ b/internal/processing/processing_suite_test.go @@ -0,0 +1,14 @@ +package processing + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/envtest/printer" + "testing" +) + +func TestProcessing(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecsWithDefaultAndCustomReporters(t, "Processing Suite", + []Reporter{printer.NewProwReporter("api-gateway-processing-testsuite")}) +} diff --git a/internal/processing/processing_test.go b/internal/processing/processing_test.go deleted file mode 100644 index 251824550..000000000 --- a/internal/processing/processing_test.go +++ /dev/null @@ -1,1551 +0,0 @@ -package processing - -import ( - "context" - "fmt" - "testing" - - "istio.io/api/networking/v1beta1" - networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" - securityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - - gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" - "github.com/kyma-incubator/api-gateway/internal/helpers" - rulev1alpha1 "github.com/ory/oathkeeper-maester/api/v1alpha1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/envtest/printer" -) - -const ( - apiName = "test-apirule" - apiUID types.UID = "eab0f1c8-c417-11e9-bf11-4ac644044351" - apiNamespace = "some-namespace" - apiAPIVersion = "gateway.kyma-project.io/v1alpha1" - apiKind = "ApiRule" - apiPath = "/.*" - headersAPIPath = "/headers" - oauthAPIPath = "/img" - jwtIssuer = "https://oauth2.example.com/" - jwksUri = "https://oauth2.example.com/.well-known/jwks.json" - jwtIssuer2 = "https://oauth2.another.example.com/" - jwksUri2 = "https://oauth2.another.example.com/.well-known/jwks.json" - oathkeeperSvc = "fake.oathkeeper" - oathkeeperSvcPort uint32 = 1234 - testLabelKey = "key" - testLabelValue = "value" - defaultDomain = "myDomain.com" - testSelectorKey = "app" -) - -var ( - config = helpers.Config{JWTHandler: helpers.JWT_HANDLER_ORY} - - apiMethods = []string{"GET"} - apiScopes = []string{"write", "read"} - servicePort uint32 = 8080 - apiGateway = "some-gateway" - serviceName = "example-service" - serviceHostWithNoDomain = "myService" - serviceHost = serviceHostWithNoDomain + "." + defaultDomain - - testAllowOrigin = []*v1beta1.StringMatch{{MatchType: &v1beta1.StringMatch_Regex{Regex: ".*"}}} - testAllowMethods = []string{"GET", "POST", "PUT", "DELETE"} - testAllowHeaders = []string{"header1", "header2"} - - testCors = &CorsConfig{ - AllowOrigins: testAllowOrigin, - AllowMethods: testAllowMethods, - AllowHeaders: testAllowHeaders, - } - - testAdditionalLabels = map[string]string{testLabelKey: testLabelValue} -) - -func TestProcessing(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecsWithDefaultAndCustomReporters(t, "Processing Suite", - []Reporter{printer.NewProwReporter("api-gateway-processing-testsuite")}) -} - -var _ = Describe("Factory", func() { - Describe("CalculateRequiredState", func() { - Context("APIRule", func() { - It("should produce VS for allow authenticator", func() { - strategies := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "allow", - }, - }, - } - - allowRule := getRuleFor(apiPath, apiMethods, []*gatewayv1beta1.Mutator{}, strategies) - rules := []gatewayv1beta1.Rule{allowRule} - - apiRule := getAPIRuleFor(rules) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &config) - vs := desiredState.virtualService - accessRules := desiredState.accessRules - - //verify VS - Expect(vs).NotTo(BeNil()) - Expect(len(vs.Spec.Gateways)).To(Equal(1)) - Expect(len(vs.Spec.Hosts)).To(Equal(1)) - Expect(vs.Spec.Hosts[0]).To(Equal(serviceHost)) - Expect(len(vs.Spec.Http)).To(Equal(1)) - - Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) - Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(serviceName + "." + apiNamespace + ".svc.cluster.local")) - Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(servicePort)) - - Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) - Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) - - Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(testCors.AllowOrigins)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(testCors.AllowMethods)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(testCors.AllowHeaders)) - - Expect(vs.ObjectMeta.Name).To(BeEmpty()) - Expect(vs.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(vs.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(vs.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - - //Verify AR - Expect(len(accessRules)).To(Equal(0)) - }) - - It("should override VS destination host for specified spec level service namespace", func() { - strategies := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "allow", - }, - }, - } - - allowRule := getRuleFor(apiPath, apiMethods, []*gatewayv1beta1.Mutator{}, strategies) - rules := []gatewayv1beta1.Rule{allowRule} - - apiRule := getAPIRuleFor(rules) - - overrideServiceName := "testName" - overrideServiceNamespace := "testName-namespace" - overrideServicePort := uint32(8080) - - apiRule.Spec.Service = &gatewayv1beta1.Service{ - Name: &overrideServiceName, - Namespace: &overrideServiceNamespace, - Port: &overrideServicePort, - } - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &config) - vs := desiredState.virtualService - accessRules := desiredState.accessRules - - //verify VS has rule level destination host - Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) - Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(overrideServiceName + "." + overrideServiceNamespace + ".svc.cluster.local")) - - //Verify AR does not exist - Expect(len(accessRules)).To(Equal(0)) - }) - - It("noop: should override access rule upstream with rule level service", func() { - strategies := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "noop", - }, - }, - } - - overrideServiceName := "testName" - overrideServicePort := uint32(8080) - - service := &gatewayv1beta1.Service{ - Name: &overrideServiceName, - Port: &overrideServicePort, - } - - allowRule := getRuleWithServiceFor(apiPath, apiMethods, []*gatewayv1beta1.Mutator{}, strategies, service) - rules := []gatewayv1beta1.Rule{allowRule} - - apiRule := getAPIRuleFor(rules) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &config) - vs := desiredState.virtualService - accessRules := desiredState.accessRules - - expectedNoopRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, apiPath) - expectedRuleUpstreamURL := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", overrideServiceName, apiNamespace, overrideServicePort) - - //Verify VS - Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) - Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(oathkeeperSvc)) - - //Verify AR has rule level upstream - Expect(len(accessRules)).To(Equal(1)) - Expect(accessRules[expectedNoopRuleMatchURL].Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL)) - }) - - It("noop: should override access rule upstream with rule level service for specified namespace", func() { - strategies := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "noop", - }, - }, - } - - overrideServiceName := "testName" - overrideServiceNamespace := "testName-namespace" - overrideServicePort := uint32(8080) - - service := &gatewayv1beta1.Service{ - Name: &overrideServiceName, - Namespace: &overrideServiceNamespace, - Port: &overrideServicePort, - } - - allowRule := getRuleWithServiceFor(apiPath, apiMethods, []*gatewayv1beta1.Mutator{}, strategies, service) - rules := []gatewayv1beta1.Rule{allowRule} - - apiRule := getAPIRuleFor(rules) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &config) - vs := desiredState.virtualService - accessRules := desiredState.accessRules - - expectedNoopRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, apiPath) - expectedRuleUpstreamURL := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", overrideServiceName, overrideServiceNamespace, overrideServicePort) - - //Verify VS - Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) - Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(oathkeeperSvc)) - - //Verify AR has rule level upstream - Expect(len(accessRules)).To(Equal(1)) - Expect(accessRules[expectedNoopRuleMatchURL].Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL)) - }) - - It("allow: should override VS destination host with rule level service", func() { - strategies := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "allow", - }, - }, - } - - overrideServiceName := "testName" - overrideServicePort := uint32(8080) - - service := &gatewayv1beta1.Service{ - Name: &overrideServiceName, - Port: &overrideServicePort, - } - - allowRule := getRuleWithServiceFor(apiPath, apiMethods, []*gatewayv1beta1.Mutator{}, strategies, service) - rules := []gatewayv1beta1.Rule{allowRule} - - apiRule := getAPIRuleFor(rules) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &config) - vs := desiredState.virtualService - accessRules := desiredState.accessRules - - //verify VS has rule level destination host - Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) - Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(overrideServiceName + "." + apiNamespace + ".svc.cluster.local")) - - //Verify AR has rule level upstream - Expect(len(accessRules)).To(Equal(0)) - }) - - It("allow: should override VS destination host with rule level service for specified namespace", func() { - strategies := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "allow", - }, - }, - } - - overrideServiceName := "testName" - overrideServiceNamespace := "testName-namespace" - overrideServicePort := uint32(8080) - - service := &gatewayv1beta1.Service{ - Name: &overrideServiceName, - Namespace: &overrideServiceNamespace, - Port: &overrideServicePort, - } - - allowRule := getRuleWithServiceFor(apiPath, apiMethods, []*gatewayv1beta1.Mutator{}, strategies, service) - rules := []gatewayv1beta1.Rule{allowRule} - - apiRule := getAPIRuleFor(rules) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &config) - vs := desiredState.virtualService - accessRules := desiredState.accessRules - - //verify VS has rule level destination host - Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) - Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(overrideServiceName + "." + overrideServiceNamespace + ".svc.cluster.local")) - - //Verify AR does not exist - Expect(len(accessRules)).To(Equal(0)) - }) - - It("should produce VS and ARs for given paths", func() { - noop := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "noop", - }, - }, - } - - jwtConfigJSON := fmt.Sprintf(` - { - "trusted_issuers": ["%s"], - "jwks": [], - "required_scope": [%s] - }`, jwtIssuer, toCSVList(apiScopes)) - - jwt := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "jwt", - Config: &runtime.RawExtension{ - Raw: []byte(jwtConfigJSON), - }, - }, - }, - } - - testMutators := []*gatewayv1beta1.Mutator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "noop", - }, - }, - { - Handler: &gatewayv1beta1.Handler{ - Name: "idtoken", - }, - }, - } - - noopRule := getRuleFor(apiPath, apiMethods, []*gatewayv1beta1.Mutator{}, noop) - jwtRule := getRuleFor(headersAPIPath, apiMethods, testMutators, jwt) - rules := []gatewayv1beta1.Rule{noopRule, jwtRule} - - expectedNoopRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, apiPath) - expectedJwtRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, headersAPIPath) - expectedRuleUpstreamURL := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", serviceName, apiNamespace, servicePort) - - apiRule := getAPIRuleFor(rules) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &config) - vs := desiredState.virtualService - accessRules := desiredState.accessRules - - //verify VS - Expect(vs).NotTo(BeNil()) - Expect(len(vs.Spec.Gateways)).To(Equal(1)) - Expect(len(vs.Spec.Hosts)).To(Equal(1)) - Expect(vs.Spec.Hosts[0]).To(Equal(serviceHost)) - Expect(len(vs.Spec.Http)).To(Equal(2)) - - Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) - Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(oathkeeperSvc)) - Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(oathkeeperSvcPort)) - Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) - Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) - - Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(testCors.AllowOrigins)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(testCors.AllowMethods)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(testCors.AllowHeaders)) - - Expect(len(vs.Spec.Http[1].Route)).To(Equal(1)) - Expect(vs.Spec.Http[1].Route[0].Destination.Host).To(Equal(oathkeeperSvc)) - Expect(vs.Spec.Http[1].Route[0].Destination.Port.Number).To(Equal(oathkeeperSvcPort)) - Expect(len(vs.Spec.Http[1].Match)).To(Equal(1)) - Expect(vs.Spec.Http[1].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[1].Path)) - - Expect(vs.Spec.Http[1].CorsPolicy.AllowOrigins).To(Equal(testCors.AllowOrigins)) - Expect(vs.Spec.Http[1].CorsPolicy.AllowMethods).To(Equal(testCors.AllowMethods)) - Expect(vs.Spec.Http[1].CorsPolicy.AllowHeaders).To(Equal(testCors.AllowHeaders)) - - Expect(vs.ObjectMeta.Name).To(BeEmpty()) - Expect(vs.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(vs.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(vs.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - - //Verify ARs - Expect(len(accessRules)).To(Equal(2)) - - noopAccessRule := accessRules[expectedNoopRuleMatchURL] - - Expect(len(accessRules)).To(Equal(2)) - Expect(len(noopAccessRule.Spec.Authenticators)).To(Equal(1)) - - Expect(noopAccessRule.Spec.Authorizer.Name).To(Equal("allow")) - Expect(noopAccessRule.Spec.Authorizer.Config).To(BeNil()) - - Expect(noopAccessRule.Spec.Authenticators[0].Handler.Name).To(Equal("noop")) - Expect(noopAccessRule.Spec.Authenticators[0].Handler.Config).To(BeNil()) - - Expect(len(noopAccessRule.Spec.Match.Methods)).To(Equal(len(apiMethods))) - Expect(noopAccessRule.Spec.Match.Methods).To(Equal(apiMethods)) - Expect(noopAccessRule.Spec.Match.URL).To(Equal(expectedNoopRuleMatchURL)) - - Expect(noopAccessRule.Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL)) - - Expect(noopAccessRule.ObjectMeta.Name).To(BeEmpty()) - Expect(noopAccessRule.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(noopAccessRule.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(noopAccessRule.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(noopAccessRule.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(noopAccessRule.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(noopAccessRule.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(noopAccessRule.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - - jwtAccessRule := accessRules[expectedJwtRuleMatchURL] - - Expect(len(jwtAccessRule.Spec.Authenticators)).To(Equal(1)) - - Expect(jwtAccessRule.Spec.Authorizer.Name).To(Equal("allow")) - Expect(jwtAccessRule.Spec.Authorizer.Config).To(BeNil()) - - Expect(jwtAccessRule.Spec.Authenticators[0].Handler.Name).To(Equal("jwt")) - Expect(jwtAccessRule.Spec.Authenticators[0].Handler.Config).NotTo(BeNil()) - Expect(string(jwtAccessRule.Spec.Authenticators[0].Handler.Config.Raw)).To(Equal(jwtConfigJSON)) - - Expect(len(jwtAccessRule.Spec.Match.Methods)).To(Equal(len(apiMethods))) - Expect(jwtAccessRule.Spec.Match.Methods).To(Equal(apiMethods)) - Expect(jwtAccessRule.Spec.Match.URL).To(Equal(expectedJwtRuleMatchURL)) - - Expect(jwtAccessRule.Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL)) - - Expect(jwtAccessRule.Spec.Mutators).NotTo(BeNil()) - Expect(len(jwtAccessRule.Spec.Mutators)).To(Equal(len(testMutators))) - Expect(jwtAccessRule.Spec.Mutators[0].Handler.Name).To(Equal(testMutators[0].Name)) - Expect(jwtAccessRule.Spec.Mutators[1].Handler.Name).To(Equal(testMutators[1].Name)) - - Expect(jwtAccessRule.ObjectMeta.Name).To(BeEmpty()) - Expect(jwtAccessRule.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(jwtAccessRule.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(jwtAccessRule.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(jwtAccessRule.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(jwtAccessRule.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(jwtAccessRule.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(jwtAccessRule.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - }) - - It("should produce one VS and two ARs for two same paths and different methods", func() { - noop := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "noop", - }, - }, - } - - jwtConfigJSON := fmt.Sprintf(` - { - "trusted_issuers": ["%s"], - "jwks": [], - "required_scope": [%s] - }`, jwtIssuer, toCSVList(apiScopes)) - - jwt := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "jwt", - Config: &runtime.RawExtension{ - Raw: []byte(jwtConfigJSON), - }, - }, - }, - } - - testMutators := []*gatewayv1beta1.Mutator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "noop", - }, - }, - { - Handler: &gatewayv1beta1.Handler{ - Name: "idtoken", - }, - }, - } - getMethod := []string{"GET"} - postMethod := []string{"POST"} - noopRule := getRuleFor(apiPath, getMethod, []*gatewayv1beta1.Mutator{}, noop) - jwtRule := getRuleFor(apiPath, postMethod, testMutators, jwt) - rules := []gatewayv1beta1.Rule{noopRule, jwtRule} - - expectedNoopRuleKey := fmt.Sprintf("://%s<%s>:%s", serviceHost, apiPath, getMethod) - expectedJwtRuleKey := fmt.Sprintf("://%s<%s>:%s", serviceHost, apiPath, postMethod) - - expectedNoopRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, apiPath) - expectedJwtRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, apiPath) - expectedRuleUpstreamURL := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", serviceName, apiNamespace, servicePort) - - apiRule := getAPIRuleFor(rules) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &config) - vs := desiredState.virtualService - accessRules := desiredState.accessRules - - //verify VS - Expect(vs).NotTo(BeNil()) - Expect(len(vs.Spec.Gateways)).To(Equal(1)) - Expect(len(vs.Spec.Hosts)).To(Equal(1)) - Expect(vs.Spec.Hosts[0]).To(Equal(serviceHost)) - Expect(len(vs.Spec.Http)).To(Equal(1)) - - Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) - Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(oathkeeperSvc)) - Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(oathkeeperSvcPort)) - Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) - Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) - - Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(testCors.AllowOrigins)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(testCors.AllowMethods)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(testCors.AllowHeaders)) - - Expect(vs.ObjectMeta.Name).To(BeEmpty()) - Expect(vs.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(vs.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(vs.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - - //Verify ARs - Expect(len(accessRules)).To(Equal(2)) - - noopAccessRule := accessRules[expectedNoopRuleKey] - - Expect(len(accessRules)).To(Equal(2)) - Expect(len(noopAccessRule.Spec.Authenticators)).To(Equal(1)) - - Expect(noopAccessRule.Spec.Authorizer.Name).To(Equal("allow")) - Expect(noopAccessRule.Spec.Authorizer.Config).To(BeNil()) - - Expect(noopAccessRule.Spec.Authenticators[0].Handler.Name).To(Equal("noop")) - Expect(noopAccessRule.Spec.Authenticators[0].Handler.Config).To(BeNil()) - - Expect(len(noopAccessRule.Spec.Match.Methods)).To(Equal(len(getMethod))) - Expect(noopAccessRule.Spec.Match.Methods).To(Equal(getMethod)) - Expect(noopAccessRule.Spec.Match.URL).To(Equal(expectedNoopRuleMatchURL)) - - Expect(noopAccessRule.Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL)) - - Expect(noopAccessRule.ObjectMeta.Name).To(BeEmpty()) - Expect(noopAccessRule.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(noopAccessRule.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(noopAccessRule.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(noopAccessRule.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(noopAccessRule.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(noopAccessRule.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(noopAccessRule.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - - jwtAccessRule := accessRules[expectedJwtRuleKey] - - Expect(len(jwtAccessRule.Spec.Authenticators)).To(Equal(1)) - - Expect(jwtAccessRule.Spec.Authorizer.Name).To(Equal("allow")) - Expect(jwtAccessRule.Spec.Authorizer.Config).To(BeNil()) - - Expect(jwtAccessRule.Spec.Authenticators[0].Handler.Name).To(Equal("jwt")) - Expect(jwtAccessRule.Spec.Authenticators[0].Handler.Config).NotTo(BeNil()) - Expect(string(jwtAccessRule.Spec.Authenticators[0].Handler.Config.Raw)).To(Equal(jwtConfigJSON)) - - Expect(len(jwtAccessRule.Spec.Match.Methods)).To(Equal(len(postMethod))) - Expect(jwtAccessRule.Spec.Match.Methods).To(Equal(postMethod)) - Expect(jwtAccessRule.Spec.Match.URL).To(Equal(expectedJwtRuleMatchURL)) - - Expect(jwtAccessRule.Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL)) - - Expect(jwtAccessRule.Spec.Mutators).NotTo(BeNil()) - Expect(len(jwtAccessRule.Spec.Mutators)).To(Equal(len(testMutators))) - Expect(jwtAccessRule.Spec.Mutators[0].Handler.Name).To(Equal(testMutators[0].Name)) - Expect(jwtAccessRule.Spec.Mutators[1].Handler.Name).To(Equal(testMutators[1].Name)) - - Expect(jwtAccessRule.ObjectMeta.Name).To(BeEmpty()) - Expect(jwtAccessRule.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(jwtAccessRule.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(jwtAccessRule.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(jwtAccessRule.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(jwtAccessRule.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(jwtAccessRule.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(jwtAccessRule.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - }) - - It("should produce VS and ARs for given two same paths and one different", func() { - noop := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "noop", - }, - }, - } - - jwtConfigJSON := fmt.Sprintf(` - { - "trusted_issuers": ["%s"], - "jwks": [], - "required_scope": [%s] - }`, jwtIssuer, toCSVList(apiScopes)) - - jwt := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "jwt", - Config: &runtime.RawExtension{ - Raw: []byte(jwtConfigJSON), - }, - }, - }, - } - - testMutators := []*gatewayv1beta1.Mutator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "noop", - }, - }, - { - Handler: &gatewayv1beta1.Handler{ - Name: "idtoken", - }, - }, - } - getMethod := []string{"GET"} - postMethod := []string{"POST"} - noopGetRule := getRuleFor(apiPath, getMethod, []*gatewayv1beta1.Mutator{}, noop) - noopPostRule := getRuleFor(apiPath, postMethod, []*gatewayv1beta1.Mutator{}, noop) - jwtRule := getRuleFor(headersAPIPath, apiMethods, testMutators, jwt) - rules := []gatewayv1beta1.Rule{noopGetRule, noopPostRule, jwtRule} - - expectedNoopGetRuleKey := fmt.Sprintf("://%s<%s>:%s", serviceHost, apiPath, getMethod) - expectedNoopPostRuleKey := fmt.Sprintf("://%s<%s>:%s", serviceHost, apiPath, postMethod) - expectedJwtRuleKey := fmt.Sprintf("://%s<%s>:%s", serviceHost, headersAPIPath, apiMethods) - - expectedNoopGetRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, apiPath) - expectedNoopPostRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, apiPath) - expectedJwtRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, headersAPIPath) - expectedRuleUpstreamURL := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", serviceName, apiNamespace, servicePort) - - apiRule := getAPIRuleFor(rules) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &config) - vs := desiredState.virtualService - accessRules := desiredState.accessRules - - //verify VS - Expect(vs).NotTo(BeNil()) - Expect(len(vs.Spec.Gateways)).To(Equal(1)) - Expect(len(vs.Spec.Hosts)).To(Equal(1)) - Expect(vs.Spec.Hosts[0]).To(Equal(serviceHost)) - Expect(len(vs.Spec.Http)).To(Equal(2)) - - Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) - Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(oathkeeperSvc)) - Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(oathkeeperSvcPort)) - Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) - Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) - - Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(testCors.AllowOrigins)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(testCors.AllowMethods)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(testCors.AllowHeaders)) - - Expect(len(vs.Spec.Http[1].Route)).To(Equal(1)) - Expect(vs.Spec.Http[1].Route[0].Destination.Host).To(Equal(oathkeeperSvc)) - Expect(vs.Spec.Http[1].Route[0].Destination.Port.Number).To(Equal(oathkeeperSvcPort)) - Expect(len(vs.Spec.Http[1].Match)).To(Equal(1)) - Expect(vs.Spec.Http[1].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[2].Path)) - - Expect(vs.Spec.Http[1].CorsPolicy.AllowOrigins).To(Equal(testCors.AllowOrigins)) - Expect(vs.Spec.Http[1].CorsPolicy.AllowMethods).To(Equal(testCors.AllowMethods)) - Expect(vs.Spec.Http[1].CorsPolicy.AllowHeaders).To(Equal(testCors.AllowHeaders)) - - Expect(vs.ObjectMeta.Name).To(BeEmpty()) - Expect(vs.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(vs.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(vs.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - - //Verify ARs - Expect(len(accessRules)).To(Equal(3)) - - noopGetAccessRule := accessRules[expectedNoopGetRuleKey] - - Expect(len(noopGetAccessRule.Spec.Authenticators)).To(Equal(1)) - - Expect(noopGetAccessRule.Spec.Authorizer.Name).To(Equal("allow")) - Expect(noopGetAccessRule.Spec.Authorizer.Config).To(BeNil()) - - Expect(noopGetAccessRule.Spec.Authenticators[0].Handler.Name).To(Equal("noop")) - Expect(noopGetAccessRule.Spec.Authenticators[0].Handler.Config).To(BeNil()) - - Expect(len(noopGetAccessRule.Spec.Match.Methods)).To(Equal(len(getMethod))) - Expect(noopGetAccessRule.Spec.Match.Methods).To(Equal(getMethod)) - Expect(noopGetAccessRule.Spec.Match.URL).To(Equal(expectedNoopGetRuleMatchURL)) - - Expect(noopGetAccessRule.Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL)) - - Expect(noopGetAccessRule.ObjectMeta.Name).To(BeEmpty()) - Expect(noopGetAccessRule.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(noopGetAccessRule.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(noopGetAccessRule.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(noopGetAccessRule.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(noopGetAccessRule.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(noopGetAccessRule.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(noopGetAccessRule.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - - noopPostAccessRule := accessRules[expectedNoopPostRuleKey] - - Expect(len(noopPostAccessRule.Spec.Authenticators)).To(Equal(1)) - - Expect(noopPostAccessRule.Spec.Authorizer.Name).To(Equal("allow")) - Expect(noopPostAccessRule.Spec.Authorizer.Config).To(BeNil()) - - Expect(noopPostAccessRule.Spec.Authenticators[0].Handler.Name).To(Equal("noop")) - Expect(noopPostAccessRule.Spec.Authenticators[0].Handler.Config).To(BeNil()) - - Expect(len(noopPostAccessRule.Spec.Match.Methods)).To(Equal(len(postMethod))) - Expect(noopPostAccessRule.Spec.Match.Methods).To(Equal(postMethod)) - Expect(noopPostAccessRule.Spec.Match.URL).To(Equal(expectedNoopPostRuleMatchURL)) - - Expect(noopPostAccessRule.Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL)) - - Expect(noopPostAccessRule.ObjectMeta.Name).To(BeEmpty()) - Expect(noopPostAccessRule.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(noopPostAccessRule.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(noopPostAccessRule.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(noopPostAccessRule.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(noopPostAccessRule.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(noopPostAccessRule.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(noopPostAccessRule.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - - jwtAccessRule := accessRules[expectedJwtRuleKey] - - Expect(len(jwtAccessRule.Spec.Authenticators)).To(Equal(1)) - - Expect(jwtAccessRule.Spec.Authorizer.Name).To(Equal("allow")) - Expect(jwtAccessRule.Spec.Authorizer.Config).To(BeNil()) - - Expect(jwtAccessRule.Spec.Authenticators[0].Handler.Name).To(Equal("jwt")) - Expect(jwtAccessRule.Spec.Authenticators[0].Handler.Config).NotTo(BeNil()) - Expect(string(jwtAccessRule.Spec.Authenticators[0].Handler.Config.Raw)).To(Equal(jwtConfigJSON)) - - Expect(len(jwtAccessRule.Spec.Match.Methods)).To(Equal(len(apiMethods))) - Expect(jwtAccessRule.Spec.Match.Methods).To(Equal(apiMethods)) - Expect(jwtAccessRule.Spec.Match.URL).To(Equal(expectedJwtRuleMatchURL)) - - Expect(jwtAccessRule.Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL)) - - Expect(jwtAccessRule.Spec.Mutators).NotTo(BeNil()) - Expect(len(jwtAccessRule.Spec.Mutators)).To(Equal(len(testMutators))) - Expect(jwtAccessRule.Spec.Mutators[0].Handler.Name).To(Equal(testMutators[0].Name)) - Expect(jwtAccessRule.Spec.Mutators[1].Handler.Name).To(Equal(testMutators[1].Name)) - - Expect(jwtAccessRule.ObjectMeta.Name).To(BeEmpty()) - Expect(jwtAccessRule.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(jwtAccessRule.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(jwtAccessRule.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(jwtAccessRule.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(jwtAccessRule.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(jwtAccessRule.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(jwtAccessRule.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - }) - - It("should produce VS & AR for jwt & oauth authenticators for given path", func() { - oauthConfigJSON := fmt.Sprintf(`{"required_scope": [%s]}`, toCSVList(apiScopes)) - - jwtConfigJSON := fmt.Sprintf(` - { - "trusted_issuers": ["%s"], - "jwks": [], - "required_scope": [%s] - }`, jwtIssuer, toCSVList(apiScopes)) - - jwt := &gatewayv1beta1.Authenticator{ - Handler: &gatewayv1beta1.Handler{ - Name: "jwt", - Config: &runtime.RawExtension{ - Raw: []byte(jwtConfigJSON), - }, - }, - } - oauth := &gatewayv1beta1.Authenticator{ - Handler: &gatewayv1beta1.Handler{ - Name: "oauth2_introspection", - Config: &runtime.RawExtension{ - Raw: []byte(oauthConfigJSON), - }, - }, - } - - strategies := []*gatewayv1beta1.Authenticator{jwt, oauth} - - allowRule := getRuleFor(apiPath, apiMethods, []*gatewayv1beta1.Mutator{}, strategies) - rules := []gatewayv1beta1.Rule{allowRule} - - expectedRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, apiPath) - expectedRuleUpstreamURL := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", serviceName, apiNamespace, servicePort) - - apiRule := getAPIRuleFor(rules) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &config) - vs := desiredState.virtualService - accessRules := desiredState.accessRules - - //verify VS - Expect(vs).NotTo(BeNil()) - Expect(len(vs.Spec.Gateways)).To(Equal(1)) - Expect(len(vs.Spec.Hosts)).To(Equal(1)) - Expect(vs.Spec.Hosts[0]).To(Equal(serviceHost)) - Expect(len(vs.Spec.Http)).To(Equal(1)) - - Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) - Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(oathkeeperSvc)) - Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(oathkeeperSvcPort)) - - Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) - Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) - - Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(testCors.AllowOrigins)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(testCors.AllowMethods)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(testCors.AllowHeaders)) - - Expect(vs.ObjectMeta.Name).To(BeEmpty()) - Expect(vs.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(vs.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(vs.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - - rule := accessRules[expectedRuleMatchURL] - - //Verify AR - Expect(len(accessRules)).To(Equal(1)) - Expect(len(rule.Spec.Authenticators)).To(Equal(2)) - - Expect(rule.Spec.Authorizer.Name).To(Equal("allow")) - Expect(rule.Spec.Authorizer.Config).To(BeNil()) - - Expect(rule.Spec.Authenticators[0].Handler.Name).To(Equal("jwt")) - Expect(rule.Spec.Authenticators[0].Handler.Config).NotTo(BeNil()) - Expect(string(rule.Spec.Authenticators[0].Handler.Config.Raw)).To(Equal(jwtConfigJSON)) - - Expect(rule.Spec.Authenticators[1].Handler.Name).To(Equal("oauth2_introspection")) - Expect(rule.Spec.Authenticators[1].Handler.Config).NotTo(BeNil()) - Expect(string(rule.Spec.Authenticators[1].Handler.Config.Raw)).To(Equal(oauthConfigJSON)) - - Expect(len(rule.Spec.Match.Methods)).To(Equal(len(apiMethods))) - Expect(rule.Spec.Match.Methods).To(Equal(apiMethods)) - Expect(rule.Spec.Match.URL).To(Equal(expectedRuleMatchURL)) - - Expect(rule.Spec.Upstream.URL).To(Equal(expectedRuleUpstreamURL)) - - Expect(rule.ObjectMeta.Name).To(BeEmpty()) - Expect(rule.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(rule.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(rule.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(rule.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(rule.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(rule.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(rule.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - - }) - - Context("when the jwt handler is istio", func() { - configIstioJWT := helpers.Config{JWTHandler: helpers.JWT_HANDLER_ISTIO} - - createIstioJwtAccessStrategy := func() *gatewayv1beta1.Authenticator { - jwtConfigJSON := fmt.Sprintf(`{ - "authentications": [{"issuer": "%s", "jwksUri": "%s"}]}`, jwtIssuer, jwksUri) - return &gatewayv1beta1.Authenticator{ - Handler: &gatewayv1beta1.Handler{ - Name: "jwt", - Config: &runtime.RawExtension{ - Raw: []byte(jwtConfigJSON), - }, - }, - } - } - - It("should produce VS, AP and RA for a rule with one issuer and two paths", func() { - - jwt := createIstioJwtAccessStrategy() - service := &gatewayv1beta1.Service{ - Name: &serviceName, - Port: &servicePort, - } - - ruleJwt := getRuleWithServiceFor(headersAPIPath, apiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}, service) - ruleJwt2 := getRuleWithServiceFor(oauthAPIPath, apiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}, service) - apiRule := getAPIRuleFor([]gatewayv1beta1.Rule{ruleJwt, ruleJwt2}) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &configIstioJWT) - vs := desiredState.virtualService - ap := desiredState.authorizationPolicies - ra := desiredState.requestAuthentications - - //verify VS - Expect(vs).NotTo(BeNil()) - Expect(len(vs.Spec.Gateways)).To(Equal(1)) - Expect(len(vs.Spec.Hosts)).To(Equal(1)) - Expect(vs.Spec.Hosts[0]).To(Equal(serviceHost)) - Expect(len(vs.Spec.Http)).To(Equal(2)) - - Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) - Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(serviceName + "." + apiNamespace + ".svc.cluster.local")) - Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(servicePort)) - Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) - Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) - - Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(testCors.AllowOrigins)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(testCors.AllowMethods)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(testCors.AllowHeaders)) - - Expect(len(vs.Spec.Http[1].Route)).To(Equal(1)) - Expect(vs.Spec.Http[1].Route[0].Destination.Host).To(Equal(serviceName + "." + apiNamespace + ".svc.cluster.local")) - Expect(vs.Spec.Http[1].Route[0].Destination.Port.Number).To(Equal(servicePort)) - Expect(len(vs.Spec.Http[1].Match)).To(Equal(1)) - Expect(vs.Spec.Http[1].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[1].Path)) - - Expect(vs.Spec.Http[1].CorsPolicy.AllowOrigins).To(Equal(testCors.AllowOrigins)) - Expect(vs.Spec.Http[1].CorsPolicy.AllowMethods).To(Equal(testCors.AllowMethods)) - Expect(vs.Spec.Http[1].CorsPolicy.AllowHeaders).To(Equal(testCors.AllowHeaders)) - - Expect(vs.ObjectMeta.Name).To(BeEmpty()) - Expect(vs.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(vs.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(vs.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - - // Verify AP and RA - Expect(len(ap)).To(Equal(2)) - Expect(len(ra)).To(Equal(1)) - - Expect(ap[headersAPIPath]).NotTo(BeNil()) - Expect(ap[headersAPIPath].ObjectMeta.Name).To(BeEmpty()) - Expect(ap[headersAPIPath].ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(ap[headersAPIPath].ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(ap[headersAPIPath].ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(ap[headersAPIPath].Spec.Selector.MatchLabels[testSelectorKey]).NotTo(BeNil()) - Expect(ap[headersAPIPath].Spec.Selector.MatchLabels[testSelectorKey]).To(Equal(serviceName)) - Expect(len(ap[headersAPIPath].Spec.Rules)).To(Equal(1)) - Expect(len(ap[headersAPIPath].Spec.Rules[0].From)).To(Equal(1)) - Expect(len(ap[headersAPIPath].Spec.Rules[0].From[0].Source.RequestPrincipals)).To(Equal(1)) - Expect(ap[headersAPIPath].Spec.Rules[0].From[0].Source.RequestPrincipals[0]).To(Equal("*")) - Expect(len(ap[headersAPIPath].Spec.Rules[0].To)).To(Equal(1)) - Expect(len(ap[headersAPIPath].Spec.Rules[0].To[0].Operation.Methods)).To(Equal(1)) - Expect(ap[headersAPIPath].Spec.Rules[0].To[0].Operation.Methods).To(ContainElements(apiMethods)) - Expect(len(ap[headersAPIPath].Spec.Rules[0].To[0].Operation.Paths)).To(Equal(1)) - Expect(ap[headersAPIPath].Spec.Rules[0].To[0].Operation.Paths).To(ContainElements(headersAPIPath)) - - Expect(ap[headersAPIPath].OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(ap[headersAPIPath].OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(ap[headersAPIPath].OwnerReferences[0].Name).To(Equal(apiName)) - Expect(ap[headersAPIPath].OwnerReferences[0].UID).To(Equal(apiUID)) - - Expect(ap[oauthAPIPath]).NotTo(BeNil()) - Expect(ap[oauthAPIPath].ObjectMeta.Name).To(BeEmpty()) - Expect(ap[oauthAPIPath].ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(ap[oauthAPIPath].ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(ap[oauthAPIPath].ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(ap[oauthAPIPath].Spec.Selector.MatchLabels[testSelectorKey]).NotTo(BeNil()) - Expect(ap[oauthAPIPath].Spec.Selector.MatchLabels[testSelectorKey]).To(Equal(serviceName)) - Expect(len(ap[oauthAPIPath].Spec.Rules)).To(Equal(1)) - Expect(len(ap[oauthAPIPath].Spec.Rules[0].From)).To(Equal(1)) - Expect(len(ap[oauthAPIPath].Spec.Rules[0].From[0].Source.RequestPrincipals)).To(Equal(1)) - Expect(ap[oauthAPIPath].Spec.Rules[0].From[0].Source.RequestPrincipals[0]).To(Equal("*")) - Expect(len(ap[oauthAPIPath].Spec.Rules[0].To)).To(Equal(1)) - Expect(len(ap[oauthAPIPath].Spec.Rules[0].To[0].Operation.Methods)).To(Equal(1)) - Expect(ap[oauthAPIPath].Spec.Rules[0].To[0].Operation.Methods).To(ContainElements(apiMethods)) - Expect(len(ap[oauthAPIPath].Spec.Rules[0].To[0].Operation.Paths)).To(Equal(1)) - Expect(ap[oauthAPIPath].Spec.Rules[0].To[0].Operation.Paths).To(ContainElements(oauthAPIPath)) - - Expect(len(ap[oauthAPIPath].OwnerReferences)).To(Equal(1)) - Expect(ap[oauthAPIPath].OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(ap[oauthAPIPath].OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(ap[oauthAPIPath].OwnerReferences[0].Name).To(Equal(apiName)) - Expect(ap[oauthAPIPath].OwnerReferences[0].UID).To(Equal(apiUID)) - - raKey := fmt.Sprintf("%s:%s", jwtIssuer, jwksUri) - Expect(ra[raKey]).NotTo(BeNil()) - Expect(ra[raKey].ObjectMeta.Name).To(BeEmpty()) - Expect(ra[raKey].ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(ra[raKey].ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(ra[raKey].ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(len(ra[raKey].OwnerReferences)).To(Equal(1)) - Expect(ra[raKey].OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(ra[raKey].OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(ra[raKey].OwnerReferences[0].Name).To(Equal(apiName)) - Expect(ra[raKey].OwnerReferences[0].UID).To(Equal(apiUID)) - - Expect(ra[raKey].Spec.Selector.MatchLabels[testSelectorKey]).NotTo(BeNil()) - Expect(ra[raKey].Spec.Selector.MatchLabels[testSelectorKey]).To(Equal(serviceName)) - Expect(len(ra[raKey].Spec.JwtRules)).To(Equal(1)) - Expect(ra[raKey].Spec.JwtRules[0].Issuer).To(Equal(jwtIssuer)) - Expect(ra[raKey].Spec.JwtRules[0].JwksUri).To(Equal(jwksUri)) - }) - - It("should produce AP and RA for a Rule without service, but service definition on ApiRule level", func() { - // given - jwt := createIstioJwtAccessStrategy() - - ruleJwt := getRuleFor(headersAPIPath, apiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}) - apiRule := getAPIRuleFor([]gatewayv1beta1.Rule{ruleJwt}) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - // when - desiredState := f.CalculateRequiredState(apiRule, &configIstioJWT) - - // then - ap := desiredState.authorizationPolicies - Expect(ap[headersAPIPath]).NotTo(BeNil()) - Expect(ap[headersAPIPath].Spec.Selector.MatchLabels[testSelectorKey]).To(Equal(serviceName)) - - ra := desiredState.requestAuthentications - raKey := fmt.Sprintf("%s:%s", jwtIssuer, jwksUri) - Expect(ra[raKey]).NotTo(BeNil()) - Expect(ra[raKey].Spec.Selector.MatchLabels[testSelectorKey]).To(Equal(serviceName)) - }) - - It("should produce AP and RA with service from Rule, when service is configured on Rule and ApiRule level", func() { - // given - jwt := createIstioJwtAccessStrategy() - - ruleServiceName := "rule-scope-example-service" - service := &gatewayv1beta1.Service{ - Name: &ruleServiceName, - Port: &servicePort, - } - - ruleJwt := getRuleWithServiceFor(headersAPIPath, apiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}, service) - apiRule := getAPIRuleFor([]gatewayv1beta1.Rule{ruleJwt}) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - // when - desiredState := f.CalculateRequiredState(apiRule, &configIstioJWT) - - // then - ap := desiredState.authorizationPolicies - Expect(ap[headersAPIPath]).NotTo(BeNil()) - Expect(ap[headersAPIPath].Spec.Selector.MatchLabels[testSelectorKey]).To(Equal(ruleServiceName)) - - ra := desiredState.requestAuthentications - raKey := fmt.Sprintf("%s:%s", jwtIssuer, jwksUri) - Expect(ra[raKey]).NotTo(BeNil()) - Expect(ra[raKey].Spec.Selector.MatchLabels[testSelectorKey]).To(Equal(ruleServiceName)) - }) - - It("should produce VS, AP and RA from a rule with two issuers and one path", func() { - jwtConfigJSON := fmt.Sprintf(`{ - "authentications": [{"issuer": "%s", "jwksUri": "%s"}, {"issuer": "%s", "jwksUri": "%s"}] - }`, jwtIssuer, jwksUri, jwtIssuer2, jwksUri2) - jwt := &gatewayv1beta1.Authenticator{ - Handler: &gatewayv1beta1.Handler{ - Name: "jwt", - Config: &runtime.RawExtension{ - Raw: []byte(jwtConfigJSON), - }, - }, - } - - service := &gatewayv1beta1.Service{ - Name: &serviceName, - Port: &servicePort, - } - - ruleJwt := getRuleWithServiceFor(headersAPIPath, apiMethods, []*gatewayv1beta1.Mutator{}, []*gatewayv1beta1.Authenticator{jwt}, service) - apiRule := getAPIRuleFor([]gatewayv1beta1.Rule{ruleJwt}) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &configIstioJWT) - vs := desiredState.virtualService - ap := desiredState.authorizationPolicies - ra := desiredState.requestAuthentications - - //verify VS - Expect(vs).NotTo(BeNil()) - Expect(len(vs.Spec.Gateways)).To(Equal(1)) - Expect(len(vs.Spec.Hosts)).To(Equal(1)) - Expect(vs.Spec.Hosts[0]).To(Equal(serviceHost)) - Expect(len(vs.Spec.Http)).To(Equal(1)) - - Expect(len(vs.Spec.Http[0].Route)).To(Equal(1)) - Expect(vs.Spec.Http[0].Route[0].Destination.Host).To(Equal(serviceName + "." + apiNamespace + ".svc.cluster.local")) - Expect(vs.Spec.Http[0].Route[0].Destination.Port.Number).To(Equal(servicePort)) - Expect(len(vs.Spec.Http[0].Match)).To(Equal(1)) - Expect(vs.Spec.Http[0].Match[0].Uri.GetRegex()).To(Equal(apiRule.Spec.Rules[0].Path)) - - Expect(vs.Spec.Http[0].CorsPolicy.AllowOrigins).To(Equal(testCors.AllowOrigins)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowMethods).To(Equal(testCors.AllowMethods)) - Expect(vs.Spec.Http[0].CorsPolicy.AllowHeaders).To(Equal(testCors.AllowHeaders)) - - Expect(vs.ObjectMeta.Name).To(BeEmpty()) - Expect(vs.ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(vs.ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(vs.ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(vs.ObjectMeta.OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(vs.ObjectMeta.OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(vs.ObjectMeta.OwnerReferences[0].Name).To(Equal(apiName)) - Expect(vs.ObjectMeta.OwnerReferences[0].UID).To(Equal(apiUID)) - - // Verify AP and RA - Expect(len(ap)).To(Equal(1)) - Expect(len(ra)).To(Equal(1)) - - Expect(ap[headersAPIPath]).NotTo(BeNil()) - Expect(ap[headersAPIPath].ObjectMeta.Name).To(BeEmpty()) - Expect(ap[headersAPIPath].ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(ap[headersAPIPath].ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(ap[headersAPIPath].ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(ap[headersAPIPath].Spec.Selector.MatchLabels[testSelectorKey]).NotTo(BeNil()) - Expect(ap[headersAPIPath].Spec.Selector.MatchLabels[testSelectorKey]).To(Equal(serviceName)) - Expect(len(ap[headersAPIPath].Spec.Rules)).To(Equal(1)) - Expect(len(ap[headersAPIPath].Spec.Rules[0].From)).To(Equal(1)) - Expect(len(ap[headersAPIPath].Spec.Rules[0].From[0].Source.RequestPrincipals)).To(Equal(1)) - Expect(ap[headersAPIPath].Spec.Rules[0].From[0].Source.RequestPrincipals[0]).To(Equal("*")) - Expect(len(ap[headersAPIPath].Spec.Rules[0].To)).To(Equal(1)) - Expect(len(ap[headersAPIPath].Spec.Rules[0].To[0].Operation.Methods)).To(Equal(1)) - Expect(ap[headersAPIPath].Spec.Rules[0].To[0].Operation.Methods).To(ContainElements(apiMethods)) - Expect(len(ap[headersAPIPath].Spec.Rules[0].To[0].Operation.Paths)).To(Equal(1)) - Expect(ap[headersAPIPath].Spec.Rules[0].To[0].Operation.Paths).To(ContainElements(headersAPIPath)) - - Expect(ap[headersAPIPath].OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(ap[headersAPIPath].OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(ap[headersAPIPath].OwnerReferences[0].Name).To(Equal(apiName)) - Expect(ap[headersAPIPath].OwnerReferences[0].UID).To(Equal(apiUID)) - - raKey := fmt.Sprintf("%s:%s%s:%s", jwtIssuer, jwksUri, jwtIssuer2, jwksUri2) - Expect(ra[raKey]).NotTo(BeNil()) - Expect(ra[raKey].ObjectMeta.Name).To(BeEmpty()) - Expect(ra[raKey].ObjectMeta.GenerateName).To(Equal(apiName + "-")) - Expect(ra[raKey].ObjectMeta.Namespace).To(Equal(apiNamespace)) - Expect(ra[raKey].ObjectMeta.Labels[testLabelKey]).To(Equal(testLabelValue)) - - Expect(len(ra[raKey].OwnerReferences)).To(Equal(1)) - Expect(ra[raKey].OwnerReferences[0].APIVersion).To(Equal(apiAPIVersion)) - Expect(ra[raKey].OwnerReferences[0].Kind).To(Equal(apiKind)) - Expect(ra[raKey].OwnerReferences[0].Name).To(Equal(apiName)) - Expect(ra[raKey].OwnerReferences[0].UID).To(Equal(apiUID)) - - Expect(ra[raKey].Spec.Selector.MatchLabels[testSelectorKey]).NotTo(BeNil()) - Expect(ra[raKey].Spec.Selector.MatchLabels[testSelectorKey]).To(Equal(serviceName)) - Expect(len(ra[raKey].Spec.JwtRules)).To(Equal(2)) - Expect(ra[raKey].Spec.JwtRules[0].Issuer).To(Equal(jwtIssuer)) - Expect(ra[raKey].Spec.JwtRules[0].JwksUri).To(Equal(jwksUri)) - Expect(ra[raKey].Spec.JwtRules[1].Issuer).To(Equal(jwtIssuer2)) - Expect(ra[raKey].Spec.JwtRules[1].JwksUri).To(Equal(jwksUri2)) - }) - }) - - Context("when the hostname does not contain domain name", func() { - It("should produce VS & AR with default domain name", func() { - noop := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "noop", - }, - }, - } - - jwtConfigJSON := fmt.Sprintf(` - { - "trusted_issuers": ["%s"], - "jwks": [], - "required_scope": [%s] - }`, jwtIssuer, toCSVList(apiScopes)) - - jwt := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "jwt", - Config: &runtime.RawExtension{ - Raw: []byte(jwtConfigJSON), - }, - }, - }, - } - - testMutators := []*gatewayv1beta1.Mutator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "noop", - }, - }, - { - Handler: &gatewayv1beta1.Handler{ - Name: "idtoken", - }, - }, - } - - noopRule := getRuleFor(apiPath, apiMethods, []*gatewayv1beta1.Mutator{}, noop) - jwtRule := getRuleFor(headersAPIPath, apiMethods, testMutators, jwt) - rules := []gatewayv1beta1.Rule{noopRule, jwtRule} - - expectedNoopRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, apiPath) - expectedJwtRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, headersAPIPath) - - apiRule := getAPIRuleFor(rules) - apiRule.Spec.Host = &serviceHostWithNoDomain - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &config) - vs := desiredState.virtualService - accessRules := desiredState.accessRules - - //verify VS - Expect(vs).NotTo(BeNil()) - Expect(len(vs.Spec.Hosts)).To(Equal(1)) - Expect(vs.Spec.Hosts[0]).To(Equal(serviceHost)) - - //Verify ARs - Expect(len(accessRules)).To(Equal(2)) - noopAccessRule := accessRules[expectedNoopRuleMatchURL] - Expect(noopAccessRule.Spec.Match.URL).To(Equal(expectedNoopRuleMatchURL)) - jwtAccessRule := accessRules[expectedJwtRuleMatchURL] - Expect(jwtAccessRule.Spec.Match.URL).To(Equal(expectedJwtRuleMatchURL)) - }) - }) - }) - }) - - Describe("CalculateRequiredState", func() { - Context("when the access rule and virtual service has owner v1alpha1 owner label", func() { - It("should get the access rule and virtual service", func() { - noop := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "noop", - }, - }, - } - - noopRule := getRuleFor(apiPath, apiMethods, []*gatewayv1beta1.Mutator{}, noop) - rules := []gatewayv1beta1.Rule{noopRule} - - apiRule := getAPIRuleFor(rules) - - rule := rulev1alpha1.Rule{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), - }, - }, - Spec: rulev1alpha1.RuleSpec{ - Match: &rulev1alpha1.Match{ - URL: "some url", - }, - }, - } - - vs := networkingv1beta1.VirtualService{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), - }, - }, - } - - scheme := runtime.NewScheme() - _ = rulev1alpha1.AddToScheme(scheme) - _ = networkingv1beta1.AddToScheme(scheme) - _ = gatewayv1beta1.AddToScheme(scheme) - _ = securityv1beta1.AddToScheme(scheme) - - client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&rule, &vs).Build() - - f := NewFactory(client, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - actualState, err := f.GetActualState(context.TODO(), apiRule, &config) - - Expect(err).To(BeNil()) - Expect(actualState.accessRules).To(HaveLen(1)) - Expect(actualState.virtualService).To(Not(BeNil())) - }) - }) - }) - - Describe("CalculateDiff", func() { - Context("between desired state & actual state", func() { - It("should produce patch containing VS to create & AR to create", func() { - noop := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "noop", - }, - }, - } - - noopRule := getRuleFor(apiPath, apiMethods, []*gatewayv1beta1.Mutator{}, noop) - rules := []gatewayv1beta1.Rule{noopRule} - - apiRule := getAPIRuleFor(rules) - expectedNoopRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, apiPath) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &config) - actualState := &State{} - - patch := f.CalculateDiff(desiredState, actualState, &config) - - //Verify patch - Expect(patch.virtualService).NotTo(BeNil()) - Expect(patch.virtualService.action).To(Equal("create")) - Expect(patch.virtualService.obj).To(Equal(desiredState.virtualService)) - - Expect(patch.accessRule).NotTo(BeNil()) - Expect(len(patch.accessRule)).To(Equal(len(desiredState.accessRules))) - Expect(patch.accessRule[expectedNoopRuleMatchURL].action).To(Equal("create")) - Expect(patch.accessRule[expectedNoopRuleMatchURL].obj).To(Equal(desiredState.accessRules[expectedNoopRuleMatchURL])) - - }) - - It("should produce patch containing VS to update, AR to create, AR to update & AR to delete", func() { - oauthConfigJSON := fmt.Sprintf(`{"required_scope": [%s]}`, toCSVList(apiScopes)) - oauth := &gatewayv1beta1.Authenticator{ - Handler: &gatewayv1beta1.Handler{ - Name: "oauth2_introspection", - Config: &runtime.RawExtension{ - Raw: []byte(oauthConfigJSON), - }, - }, - } - - strategies := []*gatewayv1beta1.Authenticator{oauth} - - noop := []*gatewayv1beta1.Authenticator{ - { - Handler: &gatewayv1beta1.Handler{ - Name: "noop", - }, - }, - } - - noopRule := getRuleFor(headersAPIPath, apiMethods, []*gatewayv1beta1.Mutator{}, noop) - allowRule := getRuleFor(oauthAPIPath, apiMethods, []*gatewayv1beta1.Mutator{}, strategies) - - rules := []gatewayv1beta1.Rule{noopRule, allowRule} - - apiRule := getAPIRuleFor(rules) - - f := NewFactory(nil, ctrl.Log.WithName("test"), oathkeeperSvc, oathkeeperSvcPort, testCors, testAdditionalLabels, defaultDomain) - - desiredState := f.CalculateRequiredState(apiRule, &config) - oauthNoopRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, oauthAPIPath) - expectedNoopRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, headersAPIPath) - notDesiredRuleMatchURL := fmt.Sprintf("://%s<%s>", serviceHost, "/delete") - - labels := make(map[string]string) - labels["myLabel"] = "should not override" - - vs := &networkingv1beta1.VirtualService{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: apiName + "-", - Labels: labels, - }, - } - - noopExistingRule := &rulev1alpha1.Rule{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: apiName + "-", - Labels: labels, - }, - Spec: rulev1alpha1.RuleSpec{ - Match: &rulev1alpha1.Match{ - URL: expectedNoopRuleMatchURL, - }, - }, - } - - deleteExistingRule := &rulev1alpha1.Rule{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: apiName + "-", - Labels: labels, - }, - Spec: rulev1alpha1.RuleSpec{ - Match: &rulev1alpha1.Match{ - URL: notDesiredRuleMatchURL, - }, - }, - } - - accessRules := make(map[string]*rulev1alpha1.Rule) - accessRules[expectedNoopRuleMatchURL] = noopExistingRule - accessRules[notDesiredRuleMatchURL] = deleteExistingRule - - actualState := &State{virtualService: vs, accessRules: accessRules} - - patch := f.CalculateDiff(desiredState, actualState, &config) - vsPatch := patch.virtualService.obj.(*networkingv1beta1.VirtualService) - - //Verify patch - Expect(patch.virtualService).NotTo(BeNil()) - Expect(patch.virtualService.action).To(Equal("update")) - Expect(vsPatch.ObjectMeta.Labels).To(Equal(actualState.virtualService.ObjectMeta.Labels)) - - //TODO verify vs spec - - Expect(len(patch.accessRule)).To(Equal(3)) - - noopPatchRule := patch.accessRule[expectedNoopRuleMatchURL] - Expect(noopPatchRule).NotTo(BeNil()) - Expect(noopPatchRule.action).To(Equal("update")) - - //TODO verify ar spec - - notDesiredPatchRule := patch.accessRule[notDesiredRuleMatchURL] - Expect(notDesiredPatchRule).NotTo(BeNil()) - Expect(notDesiredPatchRule.action).To(Equal("delete")) - - oauthPatchRule := patch.accessRule[oauthNoopRuleMatchURL] - Expect(oauthPatchRule).NotTo(BeNil()) - Expect(oauthPatchRule.action).To(Equal("create")) - - //TODO verify ar spec - }) - }) - }) -}) - -func getRuleFor(path string, methods []string, mutators []*gatewayv1beta1.Mutator, accessStrategies []*gatewayv1beta1.Authenticator) gatewayv1beta1.Rule { - return gatewayv1beta1.Rule{ - Path: path, - Methods: methods, - Mutators: mutators, - AccessStrategies: accessStrategies, - } -} - -func getRuleWithServiceFor(path string, methods []string, mutators []*gatewayv1beta1.Mutator, accessStrategies []*gatewayv1beta1.Authenticator, service *gatewayv1beta1.Service) gatewayv1beta1.Rule { - return gatewayv1beta1.Rule{ - Path: path, - Methods: methods, - Mutators: mutators, - AccessStrategies: accessStrategies, - Service: service, - } -} - -func getAPIRuleFor(rules []gatewayv1beta1.Rule) *gatewayv1beta1.APIRule { - return &gatewayv1beta1.APIRule{ - ObjectMeta: metav1.ObjectMeta{ - Name: apiName, - UID: apiUID, - Namespace: apiNamespace, - }, - TypeMeta: metav1.TypeMeta{ - APIVersion: apiAPIVersion, - Kind: apiKind, - }, - Spec: gatewayv1beta1.APIRuleSpec{ - Gateway: &apiGateway, - Service: &gatewayv1beta1.Service{ - Name: &serviceName, - Port: &servicePort, - }, - Host: &serviceHost, - Rules: rules, - }, - } -} - -func toCSVList(input []string) string { - if len(input) == 0 { - return "" - } - - res := `"` + input[0] + `"` - - for i := 1; i < len(input); i++ { - res = res + "," + `"` + input[i] + `"` - } - - return res -} diff --git a/internal/processing/reconciliation.go b/internal/processing/reconciliation.go new file mode 100644 index 000000000..9362740a1 --- /dev/null +++ b/internal/processing/reconciliation.go @@ -0,0 +1,91 @@ +package processing + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/go-logr/logr" + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/validation" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// ReconciliationCommand provides the processors and validation required to reconcile the API rule. +type ReconciliationCommand interface { + Validate(context.Context, client.Client, *gatewayv1beta1.APIRule) ([]validation.Failure, error) + // GetProcessors returns the processor relevant for the reconciliation of this command. + GetProcessors() []ReconciliationProcessor +} + +// ReconciliationProcessor provides the evaluation of changes during the reconciliation of API Rule. +type ReconciliationProcessor interface { + // EvaluateReconciliation returns the changes that needs to be applied to the cluster by comparing the desired with the actual state. + EvaluateReconciliation(context.Context, client.Client, *gatewayv1beta1.APIRule) ([]*ObjectChange, error) +} + +// Reconcile executes the reconciliation of the APIRule using the given reconciliation command. +func Reconcile(ctx context.Context, client client.Client, log *logr.Logger, cmd ReconciliationCommand, apiRule *gatewayv1beta1.APIRule) ReconciliationStatus { + + validationFailures, err := cmd.Validate(ctx, client, apiRule) + if err != nil { + // We set the status to skipped because it was not the validation that failed, but an error occurred during validation. + return GetStatusForError(log, err, gatewayv1beta1.StatusSkipped) + } + + if len(validationFailures) > 0 { + failuresJson, _ := json.Marshal(validationFailures) + log.Info(fmt.Sprintf(`Validation failure {"controller": "Api", "request": "%s/%s", "failures": %s}`, apiRule.Namespace, apiRule.Name, string(failuresJson))) + return GetFailedValidationStatus(validationFailures) + } + + for _, processor := range cmd.GetProcessors() { + + objectChanges, err := processor.EvaluateReconciliation(ctx, client, apiRule) + if err != nil { + return GetStatusForError(log, err, gatewayv1beta1.StatusSkipped) + } + + err = applyChanges(ctx, client, objectChanges...) + if err != nil { + // "We don't know exactly which object(s) are not updated properly. The safest approach is to assume nothing is correct and just use `StatusError`." + return GetStatusForError(log, err, gatewayv1beta1.StatusError) + } + } + + return getOkStatus() +} + +// applyChanges applies the given commands on the cluster +func applyChanges(ctx context.Context, client client.Client, changes ...*ObjectChange) error { + + for _, change := range changes { + err := applyChange(ctx, client, change) + if err != nil { + return err + } + } + + return nil +} + +func applyChange(ctx context.Context, client client.Client, change *ObjectChange) error { + var err error + + switch change.Action { + case create: + err = client.Create(ctx, change.Obj) + case update: + err = client.Update(ctx, change.Obj) + case delete: + err = client.Delete(ctx, change.Obj) + default: + err = fmt.Errorf("apply action %s is not supported", change.Action) + } + + if err != nil { + return err + } + + return nil +} diff --git a/internal/processing/reconciliation_test.go b/internal/processing/reconciliation_test.go new file mode 100644 index 000000000..2139f7923 --- /dev/null +++ b/internal/processing/reconciliation_test.go @@ -0,0 +1,170 @@ +package processing_test + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/builders" + "github.com/kyma-incubator/api-gateway/internal/processing" + "github.com/kyma-incubator/api-gateway/internal/validation" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var _ = Describe("Reconcile", func() { + + It("should return api status error and vs/ar status skipped when an error happens during validation", func() { + // given + cmd := MockReconciliationCommand{ + validateMock: func() ([]validation.Failure, error) { return nil, fmt.Errorf("error during validation") }, + } + client := fake.NewClientBuilder().Build() + + // when + status := processing.Reconcile(context.TODO(), client, testLogger(), cmd, &gatewayv1beta1.APIRule{}) + + // then + Expect(status.ApiRuleStatus.Code).To(Equal(gatewayv1beta1.StatusError)) + Expect(status.ApiRuleStatus.Description).To(Equal("error during validation")) + Expect(status.AccessRuleStatus.Code).To(Equal(gatewayv1beta1.StatusSkipped)) + Expect(status.VirtualServiceStatus.Code).To(Equal(gatewayv1beta1.StatusSkipped)) + }) + + It("should return api status error and vs/ar status skipped when validation failed", func() { + // given + failures := []validation.Failure{{ + AttributePath: "some.path", + Message: "The value is not allowed", + }} + cmd := MockReconciliationCommand{ + validateMock: func() ([]validation.Failure, error) { return failures, nil }, + } + client := fake.NewClientBuilder().Build() + + // when + status := processing.Reconcile(context.TODO(), client, testLogger(), cmd, &gatewayv1beta1.APIRule{}) + + // then + Expect(status.ApiRuleStatus.Code).To(Equal(gatewayv1beta1.StatusError)) + Expect(status.ApiRuleStatus.Description).To(Equal("Validation error: Attribute \"some.path\": The value is not allowed")) + Expect(status.AccessRuleStatus.Code).To(Equal(gatewayv1beta1.StatusSkipped)) + Expect(status.VirtualServiceStatus.Code).To(Equal(gatewayv1beta1.StatusSkipped)) + }) + + It("should return api status error and vs/ar status skipped when processor reconciliation returns error", func() { + // given + p := MockReconciliationProcessor{ + evaluate: func() ([]*processing.ObjectChange, error) { + return []*processing.ObjectChange{}, fmt.Errorf("error during processor execution") + }, + } + + cmd := MockReconciliationCommand{ + validateMock: func() ([]validation.Failure, error) { return []validation.Failure{}, nil }, + processorMocks: func() []processing.ReconciliationProcessor { return []processing.ReconciliationProcessor{p} }, + } + + client := fake.NewClientBuilder().Build() + + // when + status := processing.Reconcile(context.TODO(), client, testLogger(), cmd, &gatewayv1beta1.APIRule{}) + + // then + Expect(status.ApiRuleStatus.Code).To(Equal(gatewayv1beta1.StatusError)) + Expect(status.ApiRuleStatus.Description).To(Equal("error during processor execution")) + Expect(status.AccessRuleStatus.Code).To(Equal(gatewayv1beta1.StatusSkipped)) + Expect(status.VirtualServiceStatus.Code).To(Equal(gatewayv1beta1.StatusSkipped)) + }) + + It("should return api status error and vs/ar status error when error during apply of changes", func() { + // given + c := []*processing.ObjectChange{processing.NewObjectCreateAction(builders.VirtualService().Get())} + p := MockReconciliationProcessor{ + evaluate: func() ([]*processing.ObjectChange, error) { + return c, nil + }, + } + + cmd := MockReconciliationCommand{ + validateMock: func() ([]validation.Failure, error) { return []validation.Failure{}, nil }, + processorMocks: func() []processing.ReconciliationProcessor { return []processing.ReconciliationProcessor{p} }, + } + + client := fake.NewClientBuilder().Build() + + // when + status := processing.Reconcile(context.TODO(), client, testLogger(), cmd, &gatewayv1beta1.APIRule{}) + + // then + Expect(status.ApiRuleStatus.Code).To(Equal(gatewayv1beta1.StatusError)) + Expect(status.ApiRuleStatus.Description).ToNot(BeEmpty()) + Expect(status.AccessRuleStatus.Code).To(Equal(gatewayv1beta1.StatusError)) + Expect(status.VirtualServiceStatus.Code).To(Equal(gatewayv1beta1.StatusError)) + }) + + It("should return status ok for create, update and delete", func() { + // given + toBeUpdatedVs := builders.VirtualService().Name("toBeUpdated").Get() + toBeDeletedVs := builders.VirtualService().Name("toBeDeleted").Get() + c := []*processing.ObjectChange{ + processing.NewObjectCreateAction(builders.VirtualService().Name("test").Get()), + processing.NewObjectUpdateAction(toBeUpdatedVs), + processing.NewObjectDeleteAction(toBeDeletedVs), + } + p := MockReconciliationProcessor{ + evaluate: func() ([]*processing.ObjectChange, error) { + return c, nil + }, + } + + cmd := MockReconciliationCommand{ + validateMock: func() ([]validation.Failure, error) { return []validation.Failure{}, nil }, + processorMocks: func() []processing.ReconciliationProcessor { return []processing.ReconciliationProcessor{p} }, + } + + scheme := runtime.NewScheme() + err := networkingv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(toBeUpdatedVs, toBeDeletedVs).Build() + + // when + status := processing.Reconcile(context.TODO(), client, testLogger(), cmd, &gatewayv1beta1.APIRule{}) + + // then + Expect(status.ApiRuleStatus.Code).To(Equal(gatewayv1beta1.StatusOK)) + Expect(status.AccessRuleStatus.Code).To(Equal(gatewayv1beta1.StatusOK)) + Expect(status.VirtualServiceStatus.Code).To(Equal(gatewayv1beta1.StatusOK)) + }) +}) + +type MockReconciliationCommand struct { + validateMock func() ([]validation.Failure, error) + processorMocks func() []processing.ReconciliationProcessor +} + +func (r MockReconciliationCommand) Validate(_ context.Context, _ client.Client, _ *gatewayv1beta1.APIRule) ([]validation.Failure, error) { + return r.validateMock() +} + +func (r MockReconciliationCommand) GetProcessors() []processing.ReconciliationProcessor { + return r.processorMocks() +} + +type MockReconciliationProcessor struct { + evaluate func() ([]*processing.ObjectChange, error) +} + +func (r MockReconciliationProcessor) EvaluateReconciliation(_ context.Context, _ client.Client, _ *gatewayv1beta1.APIRule) ([]*processing.ObjectChange, error) { + return r.evaluate() +} + +func testLogger() *logr.Logger { + logger := ctrl.Log.WithName("test") + return &logger +} diff --git a/internal/processing/request_authentication_helpers.go b/internal/processing/request_authentication_helpers.go deleted file mode 100644 index e2768a2a9..000000000 --- a/internal/processing/request_authentication_helpers.go +++ /dev/null @@ -1,50 +0,0 @@ -package processing - -import ( - "fmt" - "istio.io/api/security/v1beta1" - - gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" - "github.com/kyma-incubator/api-gateway/internal/builders" - securityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" -) - -func modifyRequestAuthentication(existing, required *securityv1beta1.RequestAuthentication) { - existing.Spec = *required.Spec.DeepCopy() -} - -func generateRequestAuthentication(api *gatewayv1beta1.APIRule, rule gatewayv1beta1.Rule, additionalLabels map[string]string) *securityv1beta1.RequestAuthentication { - namePrefix := fmt.Sprintf("%s-", api.ObjectMeta.Name) - namespace := api.ObjectMeta.Namespace - ownerRef := generateOwnerRef(api) - - arBuilder := builders.RequestAuthenticationBuilder(). - GenerateName(namePrefix). - Namespace(namespace). - Owner(builders.OwnerReference().From(&ownerRef)). - Spec(builders.RequestAuthenticationSpecBuilder().From(generateRequestAuthenticationSpec(api, rule))). - Label(OwnerLabel, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)). - Label(OwnerLabelv1alpha1, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)) - - for k, v := range additionalLabels { - arBuilder.Label(k, v) - } - - return arBuilder.Get() -} - -func generateRequestAuthenticationSpec(api *gatewayv1beta1.APIRule, rule gatewayv1beta1.Rule) *v1beta1.RequestAuthentication { - - var serviceName string - if rule.Service != nil { - serviceName = *rule.Service.Name - } else { - serviceName = *api.Spec.Service.Name - } - - requestAuthenticationSpec := builders.RequestAuthenticationSpecBuilder(). - Selector(builders.SelectorBuilder().MatchLabels("app", serviceName)). - JwtRules(builders.JwtRuleBuilder().From(rule.AccessStrategies)) - - return requestAuthenticationSpec.Get() -} diff --git a/internal/processing/status.go b/internal/processing/status.go new file mode 100644 index 000000000..fb2c35650 --- /dev/null +++ b/internal/processing/status.go @@ -0,0 +1,96 @@ +package processing + +import ( + "fmt" + + "github.com/go-logr/logr" + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/validation" +) + +type ReconciliationStatus struct { + ApiRuleStatus *gatewayv1beta1.APIRuleResourceStatus + VirtualServiceStatus *gatewayv1beta1.APIRuleResourceStatus + AccessRuleStatus *gatewayv1beta1.APIRuleResourceStatus + RequestAuthenticationStatus *gatewayv1beta1.APIRuleResourceStatus + AuthorizationPolicyStatus *gatewayv1beta1.APIRuleResourceStatus +} + +func getStatus(apiStatus *gatewayv1beta1.APIRuleResourceStatus, statusCode gatewayv1beta1.StatusCode) ReconciliationStatus { + return ReconciliationStatus{ + ApiRuleStatus: apiStatus, + VirtualServiceStatus: &gatewayv1beta1.APIRuleResourceStatus{ + Code: statusCode, + }, AccessRuleStatus: &gatewayv1beta1.APIRuleResourceStatus{ + Code: statusCode, + }, RequestAuthenticationStatus: &gatewayv1beta1.APIRuleResourceStatus{ + Code: statusCode, + }, AuthorizationPolicyStatus: &gatewayv1beta1.APIRuleResourceStatus{ + Code: statusCode, + }, + } +} + +func getOkStatus() ReconciliationStatus { + apiRuleStatus := &gatewayv1beta1.APIRuleResourceStatus{ + Code: gatewayv1beta1.StatusOK, + } + return getStatus(apiRuleStatus, gatewayv1beta1.StatusOK) +} + +// GetStatusForError creates a status with APIRule status in error condition. Accepts an auxiliary status code that is used to report VirtualService and AccessRule status. +func GetStatusForError(log *logr.Logger, err error, statusCode gatewayv1beta1.StatusCode) ReconciliationStatus { + log.Error(err, "Error during reconciliation") + return ReconciliationStatus{ + ApiRuleStatus: generateErrorStatus(err), + VirtualServiceStatus: &gatewayv1beta1.APIRuleResourceStatus{ + Code: statusCode, + }, AccessRuleStatus: &gatewayv1beta1.APIRuleResourceStatus{ + Code: statusCode, + }, RequestAuthenticationStatus: &gatewayv1beta1.APIRuleResourceStatus{ + Code: statusCode, + }, AuthorizationPolicyStatus: &gatewayv1beta1.APIRuleResourceStatus{ + Code: statusCode, + }, + } +} + +func generateErrorStatus(err error) *gatewayv1beta1.APIRuleResourceStatus { + return toStatus(gatewayv1beta1.StatusError, err.Error()) +} + +func GetFailedValidationStatus(failures []validation.Failure) ReconciliationStatus { + apiRuleStatus := generateValidationStatus(failures) + return getStatus(apiRuleStatus, gatewayv1beta1.StatusSkipped) +} + +func generateValidationStatus(failures []validation.Failure) *gatewayv1beta1.APIRuleResourceStatus { + return toStatus(gatewayv1beta1.StatusError, generateValidationDescription(failures)) +} + +func generateValidationDescription(failures []validation.Failure) string { + var description string + + if len(failures) == 1 { + description = "Validation error: " + description += fmt.Sprintf("Attribute \"%s\": %s", failures[0].AttributePath, failures[0].Message) + } else { + const maxEntries = 3 + description = "Multiple validation errors: " + for i := 0; i < len(failures) && i < maxEntries; i++ { + description += fmt.Sprintf("\nAttribute \"%s\": %s", failures[i].AttributePath, failures[i].Message) + } + if len(failures) > maxEntries { + description += fmt.Sprintf("\n%d more error(s)...", len(failures)-maxEntries) + } + } + + return description +} + +func toStatus(c gatewayv1beta1.StatusCode, desc string) *gatewayv1beta1.APIRuleResourceStatus { + return &gatewayv1beta1.APIRuleResourceStatus{ + Code: c, + Description: desc, + } +} diff --git a/controllers/validation_status_test.go b/internal/processing/status_test.go similarity index 88% rename from controllers/validation_status_test.go rename to internal/processing/status_test.go index 166244283..783770dbf 100644 --- a/controllers/validation_status_test.go +++ b/internal/processing/status_test.go @@ -1,4 +1,4 @@ -package controllers +package processing import ( "strings" @@ -9,8 +9,8 @@ import ( . "github.com/onsi/gomega" ) -var _ = Describe("Controller", func() { - Describe("generateValidationProblemStatus", func() { +var _ = Describe("Status", func() { + Context("generateValidationStatus", func() { f1 := validation.Failure{AttributePath: "name", Message: "is wrong"} f2 := validation.Failure{AttributePath: "gateway", Message: "is bad"} @@ -18,7 +18,7 @@ var _ = Describe("Controller", func() { f4 := validation.Failure{AttributePath: "service.port", Message: "is too big"} f5 := validation.Failure{AttributePath: "service.host", Message: "is invalid"} - It("should genereate status for single failure", func() { + It("should generate status for single failure", func() { failures := []validation.Failure{f1} st := generateValidationStatus(failures) @@ -30,7 +30,7 @@ var _ = Describe("Controller", func() { Expect(failureLines[0]).To(HaveSuffix("Attribute \"name\": is wrong")) }) - It("should genereate status for three failures", func() { + It("should generate status for three failures", func() { failures := []validation.Failure{f1, f2, f3} st := generateValidationStatus(failures) @@ -44,7 +44,7 @@ var _ = Describe("Controller", func() { Expect(failureLines[3]).To(Equal("Attribute \"service.name\": is too short")) }) - It("should genereate status for five failures", func() { + It("should generate status for five failures", func() { failures := []validation.Failure{f1, f2, f3, f4, f5} st := generateValidationStatus(failures) diff --git a/internal/processing/types.go b/internal/processing/types.go new file mode 100644 index 000000000..9255ecb40 --- /dev/null +++ b/internal/processing/types.go @@ -0,0 +1,70 @@ +package processing + +import ( + v1beta12 "istio.io/api/networking/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Action int + +const ( + create Action = iota + update + delete +) + +func (s Action) String() string { + switch s { + case create: + return "create" + case update: + return "update" + case delete: + return "delete" + } + return "unknown" +} + +type ObjectChange struct { + Action Action + Obj client.Object +} + +func NewObjectCreateAction(obj client.Object) *ObjectChange { + return &ObjectChange{ + Action: create, + Obj: obj, + } +} + +func NewObjectUpdateAction(obj client.Object) *ObjectChange { + return &ObjectChange{ + Action: update, + Obj: obj, + } +} + +func NewObjectDeleteAction(obj client.Object) *ObjectChange { + return &ObjectChange{ + Action: delete, + Obj: obj, + } +} + +// CorsConfig is an internal representation of v1alpha3.CorsPolicy object +type CorsConfig struct { + AllowOrigins []*v1beta12.StringMatch + AllowMethods []string + AllowHeaders []string +} + +type ReconciliationConfig struct { + OathkeeperSvc string + OathkeeperSvcPort uint32 + CorsConfig *CorsConfig + AdditionalLabels map[string]string + DefaultDomainName string + ServiceBlockList map[string][]string + DomainAllowList []string + HostBlockList []string +} diff --git a/internal/processing/virtual_service_helpers.go b/internal/processing/virtual_service_helpers.go deleted file mode 100644 index 8a0b3b8ef..000000000 --- a/internal/processing/virtual_service_helpers.go +++ /dev/null @@ -1,81 +0,0 @@ -package processing - -import ( - "fmt" - - "github.com/kyma-incubator/api-gateway/internal/helpers" - networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" - - gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" - "github.com/kyma-incubator/api-gateway/internal/builders" -) - -func (f *Factory) updateVirtualService(existing, required *networkingv1beta1.VirtualService) { - existing.Spec = *required.Spec.DeepCopy() -} - -func (f *Factory) generateVirtualService(api *gatewayv1beta1.APIRule, config *helpers.Config) *networkingv1beta1.VirtualService { - virtualServiceNamePrefix := fmt.Sprintf("%s-", api.ObjectMeta.Name) - ownerRef := generateOwnerRef(api) - - vsSpecBuilder := builders.VirtualServiceSpec() - vsSpecBuilder.Host(helpers.GetHostWithDomain(*api.Spec.Host, f.defaultDomainName)) - vsSpecBuilder.Gateway(*api.Spec.Gateway) - filteredRules := filterDuplicatePaths(api.Spec.Rules) - - for _, rule := range filteredRules { - httpRouteBuilder := builders.HTTPRoute() - serviceNamespace := helpers.FindServiceNamespace(api, &rule) - - routeDirectlyToService := false - - if !isSecured(rule) { - routeDirectlyToService = true - } else if isJwtSecured(rule) && config.JWTHandler == helpers.JWT_HANDLER_ISTIO { - routeDirectlyToService = true - } - - var host string - var port uint32 - - if routeDirectlyToService { - // Use rule level service if it exists - if rule.Service != nil { - host = helpers.GetHostLocalDomain(*rule.Service.Name, *serviceNamespace) - port = *rule.Service.Port - } else { - // Otherwise use service defined on APIRule spec level - host = helpers.GetHostLocalDomain(*api.Spec.Service.Name, *serviceNamespace) - port = *api.Spec.Service.Port - } - } else { - host = f.oathkeeperSvc - port = f.oathkeeperSvcPort - } - - httpRouteBuilder.Route(builders.RouteDestination().Host(host).Port(port)) - httpRouteBuilder.Match(builders.MatchRequest().Uri().Regex(rule.Path)) - httpRouteBuilder.CorsPolicy(builders.CorsPolicy(). - AllowOrigins(f.corsConfig.AllowOrigins...). - AllowMethods(f.corsConfig.AllowMethods...). - AllowHeaders(f.corsConfig.AllowHeaders...)) - httpRouteBuilder.Headers(builders.Headers(). - SetHostHeader(helpers.GetHostWithDomain(*api.Spec.Host, f.defaultDomainName))) - vsSpecBuilder.HTTP(httpRouteBuilder) - } - - vsBuilder := builders.VirtualService(). - GenerateName(virtualServiceNamePrefix). - Namespace(api.ObjectMeta.Namespace). - Owner(builders.OwnerReference().From(&ownerRef)). - Label(OwnerLabel, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)). - Label(OwnerLabelv1alpha1, fmt.Sprintf("%s.%s", api.ObjectMeta.Name, api.ObjectMeta.Namespace)) - - for k, v := range f.additionalLabels { - vsBuilder.Label(k, v) - } - - vsBuilder.Spec(vsSpecBuilder) - - return vsBuilder.Get() -} diff --git a/internal/processing/virtual_service_processor.go b/internal/processing/virtual_service_processor.go new file mode 100644 index 000000000..b80fe3cb3 --- /dev/null +++ b/internal/processing/virtual_service_processor.go @@ -0,0 +1,58 @@ +package processing + +import ( + "context" + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +// VirtualServiceProcessor is the generic processor that handles the Virtual Service in the reconciliation of API Rule. +type VirtualServiceProcessor struct { + Creator VirtualServiceCreator +} + +// VirtualServiceCreator provides the creation of a Virtual Service using the configuration in the given APIRule. +type VirtualServiceCreator interface { + Create(api *gatewayv1beta1.APIRule) *networkingv1beta1.VirtualService +} + +func (r VirtualServiceProcessor) EvaluateReconciliation(ctx context.Context, client ctrlclient.Client, apiRule *gatewayv1beta1.APIRule) ([]*ObjectChange, error) { + desired := r.getDesiredState(apiRule) + actual, err := r.getActualState(ctx, client, apiRule) + if err != nil { + return make([]*ObjectChange, 0), err + } + + changes := r.getObjectChanges(desired, actual) + + return []*ObjectChange{changes}, nil +} + +func (r VirtualServiceProcessor) getDesiredState(api *gatewayv1beta1.APIRule) *networkingv1beta1.VirtualService { + return r.Creator.Create(api) +} + +func (r VirtualServiceProcessor) getActualState(ctx context.Context, client ctrlclient.Client, api *gatewayv1beta1.APIRule) (*networkingv1beta1.VirtualService, error) { + labels := GetOwnerLabels(api) + + var vsList networkingv1beta1.VirtualServiceList + if err := client.List(ctx, &vsList, ctrlclient.MatchingLabels(labels)); err != nil { + return nil, err + } + + if len(vsList.Items) >= 1 { + return vsList.Items[0], nil + } else { + return nil, nil + } +} + +func (r VirtualServiceProcessor) getObjectChanges(desiredVs *networkingv1beta1.VirtualService, actualVs *networkingv1beta1.VirtualService) *ObjectChange { + if actualVs != nil { + actualVs.Spec = *desiredVs.Spec.DeepCopy() + return NewObjectUpdateAction(actualVs) + } else { + return NewObjectCreateAction(desiredVs) + } +} diff --git a/internal/processing/virtual_service_processor_test.go b/internal/processing/virtual_service_processor_test.go new file mode 100644 index 000000000..625c21e7b --- /dev/null +++ b/internal/processing/virtual_service_processor_test.go @@ -0,0 +1,84 @@ +package processing_test + +import ( + "context" + "fmt" + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" + "github.com/kyma-incubator/api-gateway/internal/builders" + "github.com/kyma-incubator/api-gateway/internal/processing" + . "github.com/kyma-incubator/api-gateway/internal/processing/internal/test" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var _ = Describe("Virtual Service Processor", func() { + It("should create virtual service when no virtual service exists", func() { + // given + apiRule := &gatewayv1beta1.APIRule{} + + processor := processing.VirtualServiceProcessor{ + Creator: mockVirtualServiceCreator{}, + } + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), GetEmptyFakeClient(), apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Action.String()).To(Equal("create")) + }) + + It("should update virtual service when virtual service exists", func() { + // given + strategies := []*gatewayv1beta1.Authenticator{ + { + Handler: &gatewayv1beta1.Handler{ + Name: "allow", + }, + }, + } + + allowRule := GetRuleFor(ApiPath, ApiMethods, []*gatewayv1beta1.Mutator{}, strategies) + rules := []gatewayv1beta1.Rule{allowRule} + + apiRule := GetAPIRuleFor(rules) + + vs := networkingv1beta1.VirtualService{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + processing.OwnerLabelv1alpha1: fmt.Sprintf("%s.%s", apiRule.ObjectMeta.Name, apiRule.ObjectMeta.Namespace), + }, + }, + } + + scheme := runtime.NewScheme() + err := networkingv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&vs).Build() + + processor := processing.VirtualServiceProcessor{ + Creator: mockVirtualServiceCreator{}, + } + + // when + result, err := processor.EvaluateReconciliation(context.TODO(), client, apiRule) + + // then + Expect(err).To(BeNil()) + Expect(result).To(HaveLen(1)) + Expect(result[0].Action.String()).To(Equal("update")) + }) +}) + +type mockVirtualServiceCreator struct { +} + +func (r mockVirtualServiceCreator) Create(_ *gatewayv1beta1.APIRule) *networkingv1beta1.VirtualService { + return builders.VirtualService().Get() +} diff --git a/internal/validation/dummy.go b/internal/validation/dummy.go index 5e1264f1b..6ea31ecab 100644 --- a/internal/validation/dummy.go +++ b/internal/validation/dummy.go @@ -2,12 +2,11 @@ package validation import ( gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" - "github.com/kyma-incubator/api-gateway/internal/helpers" ) // dummy is an accessStrategy validator that does nothing type dummyAccStrValidator struct{} -func (dummy *dummyAccStrValidator) Validate(attrPath string, handler *gatewayv1beta1.Handler, config *helpers.Config) []Failure { +func (dummy *dummyAccStrValidator) Validate(attrPath string, handler *gatewayv1beta1.Handler) []Failure { return nil } diff --git a/internal/validation/helpers.go b/internal/validation/helpers.go index 7fde0bccc..61e0bd044 100644 --- a/internal/validation/helpers.go +++ b/internal/validation/helpers.go @@ -1,12 +1,15 @@ package validation import ( + "bytes" "errors" "fmt" "net/url" "regexp" "strings" + "k8s.io/apimachinery/pkg/runtime" + gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" ) @@ -35,7 +38,7 @@ func hasPathAndMethodDuplicates(rules []gatewayv1beta1.Rule) bool { return false } -func isInvalidURL(toTest string) (bool, error) { +func IsInvalidURL(toTest string) (bool, error) { if len(toTest) == 0 { return true, errors.New("value is empty") } @@ -46,7 +49,7 @@ func isInvalidURL(toTest string) (bool, error) { return false, nil } -func isUnsecuredURL(toTest string) (bool, error) { +func IsUnsecuredURL(toTest string) (bool, error) { if len(toTest) == 0 { return true, errors.New("value is empty") } @@ -78,3 +81,17 @@ func validateGatewayName(gateway string) bool { regExp := regexp.MustCompile(`^[0-9a-z-_]+(\/[0-9a-z-_]+|(\.[0-9a-z-_]+)*)$`) return regExp.MatchString(gateway) } + +// configNotEmpty Verify if the config object is not empty +func configEmpty(config *runtime.RawExtension) bool { + + return config == nil || + len(config.Raw) == 0 || + bytes.Equal(config.Raw, []byte("null")) || + bytes.Equal(config.Raw, []byte("{}")) +} + +// configNotEmpty Verify if the config object is not empty +func ConfigNotEmpty(config *runtime.RawExtension) bool { + return !configEmpty(config) +} diff --git a/internal/validation/jwt.go b/internal/validation/jwt.go deleted file mode 100644 index d088de0a1..000000000 --- a/internal/validation/jwt.go +++ /dev/null @@ -1,111 +0,0 @@ -package validation - -import ( - "encoding/json" - "fmt" - - gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" - "github.com/kyma-incubator/api-gateway/internal/helpers" - istioint "github.com/kyma-incubator/api-gateway/internal/types/istio" - oryint "github.com/kyma-incubator/api-gateway/internal/types/ory" -) - -// jwtAccStrValidator is an accessStrategy validator for jwt ORY authenticator -type jwtAccStrValidator struct{} - -func (j *jwtAccStrValidator) Validate(attrPath string, handler *gatewayv1beta1.Handler, config *helpers.Config) []Failure { - var problems []Failure - - if !configNotEmpty(handler.Config) { - problems = append(problems, Failure{AttributePath: attrPath + ".config", Message: "supplied config cannot be empty"}) - return problems - } - - switch config.JWTHandler { - case helpers.JWT_HANDLER_ORY: - problems = j.validateOryConfig(attrPath, handler) - case helpers.JWT_HANDLER_ISTIO: - problems = j.validateIstioConfig(attrPath, handler) - } - - return problems -} - -func (j *jwtAccStrValidator) validateOryConfig(attrPath string, handler *gatewayv1beta1.Handler) []Failure { - var problems []Failure - var template oryint.JWTAccStrConfig - - err := json.Unmarshal(handler.Config.Raw, &template) - if err != nil { - problems = append(problems, Failure{AttributePath: attrPath + ".config", Message: "Can't read json: " + err.Error()}) - return problems - } - - if len(template.TrustedIssuers) > 0 { - for i := 0; i < len(template.TrustedIssuers); i++ { - invalid, err := isInvalidURL(template.TrustedIssuers[i]) - if invalid { - attrPath := fmt.Sprintf("%s[%d]", attrPath+".config.trusted_issuers", i) - problems = append(problems, Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is empty or not a valid url err=%s", err)}) - } - unsecured, err := isUnsecuredURL(template.TrustedIssuers[i]) - if unsecured { - attrPath := fmt.Sprintf("%s[%d]", attrPath+".config.trusted_issuers", i) - problems = append(problems, Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is not a secured url err=%s", err)}) - } - } - } - - if len(template.JWKSUrls) > 0 { - for i := 0; i < len(template.JWKSUrls); i++ { - invalid, err := isInvalidURL(template.JWKSUrls[i]) - if invalid { - attrPath := fmt.Sprintf("%s[%d]", attrPath+".config.jwks_urls", i) - problems = append(problems, Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is empty or not a valid url err=%s", err)}) - } - unsecured, err := isUnsecuredURL(template.JWKSUrls[i]) - if unsecured { - attrPath := fmt.Sprintf("%s[%d]", attrPath+".config.jwks_urls", i) - problems = append(problems, Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is not a secured url err=%s", err)}) - } - } - } - - return problems -} - -func (j *jwtAccStrValidator) validateIstioConfig(attrPath string, handler *gatewayv1beta1.Handler) []Failure { - var problems []Failure - var template istioint.JwtConfig - - err := json.Unmarshal(handler.Config.Raw, &template) - if err != nil { - problems = append(problems, Failure{AttributePath: attrPath + ".config", Message: "Can't read json: " + err.Error()}) - return problems - } - - for i, auth := range template.Authentications { - invalidIssuer, err := isInvalidURL(auth.Issuer) - if invalidIssuer { - attrPath := fmt.Sprintf("%s%s[%d]%s", attrPath, ".config.authentications", i, ".issuer") - problems = append(problems, Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is empty or not a valid url err=%s", err)}) - } - unsecuredIssuer, err := isUnsecuredURL(auth.Issuer) - if unsecuredIssuer { - attrPath := fmt.Sprintf("%s%s[%d]%s", attrPath, ".config.authentications", i, ".issuer") - problems = append(problems, Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is not a secured url err=%s", err)}) - } - invalidJwksUri, err := isInvalidURL(auth.JwksUri) - if invalidJwksUri { - attrPath := fmt.Sprintf("%s%s[%d]%s", attrPath, ".config.authentications", i, ".jwksUri") - problems = append(problems, Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is empty or not a valid url err=%s", err)}) - } - unsecuredJwksUri, err := isUnsecuredURL(auth.JwksUri) - if unsecuredJwksUri { - attrPath := fmt.Sprintf("%s%s[%d]%s", attrPath, ".config.authentications", i, ".jwksUri") - problems = append(problems, Failure{AttributePath: attrPath, Message: fmt.Sprintf("value is not a secured url err=%s", err)}) - } - } - - return problems -} diff --git a/internal/validation/labels.go b/internal/validation/labels.go index 9e7d23dc7..da2b67f3a 100644 --- a/internal/validation/labels.go +++ b/internal/validation/labels.go @@ -10,7 +10,6 @@ import ( const ( labelKeyPrefixRegexDef = "^[a-z]{1,}(([.][a-z]){0,}([-]?[a-z0-9]{1,}){0,}){0,}$" //"prefix" part of k8s label key (before "/") labelKeyNameRegexDef = "^[a-zA-Z0-9]([-A-Za-z0-9_.]{0,61}[a-zA-Z0-9]){0,}$" //"name" part of k8s label key (after "/") - labelValueRegexpDef = labelKeyNameRegexDef //value of k8s label. The only difference is this one can be empty ) var ( diff --git a/internal/validation/no_config.go b/internal/validation/no_config.go index 5fc2777bb..6b24dc5e9 100644 --- a/internal/validation/no_config.go +++ b/internal/validation/no_config.go @@ -4,13 +4,12 @@ import ( "bytes" gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" - "github.com/kyma-incubator/api-gateway/internal/helpers" ) // noConfig is an accessStrategy validator that does not accept nested config type noConfigAccStrValidator struct{} -func (a *noConfigAccStrValidator) Validate(attrPath string, handler *gatewayv1beta1.Handler, config *helpers.Config) []Failure { +func (a *noConfigAccStrValidator) Validate(attrPath string, handler *gatewayv1beta1.Handler) []Failure { var problems []Failure if handler.Config != nil && len(handler.Config.Raw) > 0 && !bytes.Equal(handler.Config.Raw, []byte("null")) && !bytes.Equal(handler.Config.Raw, []byte("{}")) { diff --git a/internal/validation/validate.go b/internal/validation/validate.go index 41ea4368c..4ea2c6df3 100644 --- a/internal/validation/validate.go +++ b/internal/validation/validate.go @@ -1,7 +1,6 @@ package validation import ( - "bytes" "fmt" "strings" @@ -9,35 +8,20 @@ import ( networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/strings/slices" ) // Validators for AccessStrategies var vldNoConfig = &noConfigAccStrValidator{} -var vldJWT = &jwtAccStrValidator{} var vldDummy = &dummyAccStrValidator{} type accessStrategyValidator interface { - Validate(attrPath string, Handler *gatewayv1beta1.Handler, config *helpers.Config) []Failure -} - -// configNotEmpty Verify if the config object is not empty -func configEmpty(config *runtime.RawExtension) bool { - - return config == nil || - len(config.Raw) == 0 || - bytes.Equal(config.Raw, []byte("null")) || - bytes.Equal(config.Raw, []byte("{}")) -} - -// configNotEmpty Verify if the config object is not empty -func configNotEmpty(config *runtime.RawExtension) bool { - return !configEmpty(config) + Validate(attrPath string, Handler *gatewayv1beta1.Handler) []Failure } // APIRule is used to validate github.com/kyma-incubator/api-gateway/api/v1beta1/APIRule instances type APIRule struct { + JwtValidator accessStrategyValidator ServiceBlockList map[string][]string DomainAllowList []string HostBlockList []string @@ -51,7 +35,7 @@ type Failure struct { } // Validate performs APIRule validation -func (v *APIRule) Validate(api *gatewayv1beta1.APIRule, vsList networkingv1beta1.VirtualServiceList, config *helpers.Config) []Failure { +func (v *APIRule) Validate(api *gatewayv1beta1.APIRule, vsList networkingv1beta1.VirtualServiceList) []Failure { res := []Failure{} //Validate service on path level if it is created @@ -63,7 +47,7 @@ func (v *APIRule) Validate(api *gatewayv1beta1.APIRule, vsList networkingv1beta1 //Validate Gateway res = append(res, v.validateGateway(".spec.gateway", api.Spec.Gateway)...) //Validate Rules - res = append(res, v.validateRules(".spec.rules", api.Spec.Service == nil, api, config)...) + res = append(res, v.validateRules(".spec.rules", api.Spec.Service == nil, api)...) return res } @@ -164,7 +148,7 @@ func (v *APIRule) validateGateway(attributePath string, gateway *string) []Failu // Validates whether all rules are defined correctly // Checks whether all rules have service defined for them if checkForService is true -func (v *APIRule) validateRules(attributePath string, checkForService bool, api *gatewayv1beta1.APIRule, config *helpers.Config) []Failure { +func (v *APIRule) validateRules(attributePath string, checkForService bool, api *gatewayv1beta1.APIRule) []Failure { var problems []Failure rules := api.Spec.Rules @@ -180,7 +164,7 @@ func (v *APIRule) validateRules(attributePath string, checkForService bool, api for i, r := range rules { attributePathWithRuleIndex := fmt.Sprintf("%s[%d]", attributePath, i) problems = append(problems, v.validateMethods(attributePathWithRuleIndex+".methods", r.Methods)...) - problems = append(problems, v.validateAccessStrategies(attributePathWithRuleIndex+".accessStrategies", r.AccessStrategies, config)...) + problems = append(problems, v.validateAccessStrategies(attributePathWithRuleIndex+".accessStrategies", r.AccessStrategies)...) if checkForService && r.Service == nil { problems = append(problems, Failure{AttributePath: attributePathWithRuleIndex + ".service", Message: "No service defined with no main service on spec level"}) } @@ -206,7 +190,7 @@ func (v *APIRule) validateMethods(attributePath string, methods []string) []Fail return nil } -func (v *APIRule) validateAccessStrategies(attributePath string, accessStrategies []*gatewayv1beta1.Authenticator, config *helpers.Config) []Failure { +func (v *APIRule) validateAccessStrategies(attributePath string, accessStrategies []*gatewayv1beta1.Authenticator) []Failure { var problems []Failure if len(accessStrategies) == 0 { @@ -216,13 +200,13 @@ func (v *APIRule) validateAccessStrategies(attributePath string, accessStrategie for i, r := range accessStrategies { strategyAttrPath := attributePath + fmt.Sprintf("[%d]", i) - problems = append(problems, v.validateAccessStrategy(strategyAttrPath, r, config)...) + problems = append(problems, v.validateAccessStrategy(strategyAttrPath, r)...) } return problems } -func (v *APIRule) validateAccessStrategy(attributePath string, accessStrategy *gatewayv1beta1.Authenticator, config *helpers.Config) []Failure { +func (v *APIRule) validateAccessStrategy(attributePath string, accessStrategy *gatewayv1beta1.Authenticator) []Failure { var problems []Failure var vld accessStrategyValidator @@ -243,13 +227,13 @@ func (v *APIRule) validateAccessStrategy(attributePath string, accessStrategy *g case "oauth2_introspection": vld = vldDummy case "jwt": - vld = vldJWT + vld = v.JwtValidator default: problems = append(problems, Failure{AttributePath: attributePath + ".handler", Message: fmt.Sprintf("Unsupported accessStrategy: %s", accessStrategy.Handler.Name)}) return problems } - return vld.Validate(attributePath, accessStrategy.Handler, config) + return vld.Validate(attributePath, accessStrategy.Handler) } func occupiesHost(vs *networkingv1beta1.VirtualService, host string) bool { diff --git a/internal/validation/validate_test.go b/internal/validation/validate_test.go index be543739a..c568140be 100644 --- a/internal/validation/validate_test.go +++ b/internal/validation/validate_test.go @@ -13,8 +13,7 @@ import ( gatewayv1beta1 "github.com/kyma-incubator/api-gateway/api/v1beta1" "github.com/kyma-incubator/api-gateway/internal/helpers" - istioint "github.com/kyma-incubator/api-gateway/internal/types/istio" - oryint "github.com/kyma-incubator/api-gateway/internal/types/ory" + "github.com/kyma-incubator/api-gateway/internal/types/ory" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/runtime" @@ -36,7 +35,7 @@ const ( var ( testDomainAllowlist = []string{"foo.bar", "bar.foo", "kyma.local"} - config = helpers.Config{JWTHandler: helpers.JWT_HANDLER_ORY} + jwtValidatorMock = &dummyAccStrValidator{} ) var _ = Describe("ValidateConfig function", func() { @@ -82,7 +81,7 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ DomainAllowList: testAllowList, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(1)) @@ -117,9 +116,10 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ + JwtValidator: jwtValidatorMock, ServiceBlockList: testBlockList, DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(1)) @@ -155,9 +155,10 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ + JwtValidator: jwtValidatorMock, ServiceBlockList: testBlockList, DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(1)) @@ -188,9 +189,10 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ + JwtValidator: jwtValidatorMock, ServiceBlockList: testBlockList, DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(1)) @@ -223,11 +225,12 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ + JwtValidator: jwtValidatorMock, ServiceBlockList: testBlockList, DomainAllowList: testDomainAllowlist, HostBlockList: testHostBlockList, DefaultDomainName: testDefaultDomain, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(1)) @@ -262,11 +265,12 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ + JwtValidator: jwtValidatorMock, ServiceBlockList: testBlockList, DomainAllowList: testDomainAllowlist, HostBlockList: testHostBlockList, DefaultDomainName: testDefaultDomain, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(0)) @@ -295,9 +299,10 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ + JwtValidator: jwtValidatorMock, ServiceBlockList: testBlockList, DomainAllowList: []string{}, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(0)) @@ -326,9 +331,10 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ + JwtValidator: jwtValidatorMock, ServiceBlockList: testBlockList, DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(1)) @@ -359,9 +365,10 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ + JwtValidator: jwtValidatorMock, ServiceBlockList: testBlockList, DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(1)) @@ -392,10 +399,11 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ + JwtValidator: jwtValidatorMock, ServiceBlockList: testBlockList, DomainAllowList: testDomainAllowlist, DefaultDomainName: testDefaultDomain, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(0)) @@ -424,9 +432,10 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ + JwtValidator: jwtValidatorMock, ServiceBlockList: testBlockList, DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(1)) @@ -462,8 +471,9 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ + JwtValidator: jwtValidatorMock, DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{Items: []*networkingv1beta1.VirtualService{&existingVS}}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{Items: []*networkingv1beta1.VirtualService{&existingVS}}) Expect(problems).To(HaveLen(1)) Expect(problems[0].AttributePath).To(Equal(".spec.host")) @@ -498,8 +508,9 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ + JwtValidator: jwtValidatorMock, DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{Items: []*networkingv1beta1.VirtualService{&existingVS}}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{Items: []*networkingv1beta1.VirtualService{&existingVS}}) Expect(problems).To(HaveLen(0)) }) @@ -522,7 +533,7 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(1)) @@ -566,7 +577,7 @@ var _ = Describe("Validate function", func() { problems := (&APIRule{ ServiceBlockList: testBlockList, DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(1)) @@ -611,7 +622,7 @@ var _ = Describe("Validate function", func() { problems := (&APIRule{ ServiceBlockList: testBlockList, DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(1)) @@ -630,7 +641,6 @@ var _ = Describe("Validate function", func() { Path: "/abc", AccessStrategies: []*gatewayv1beta1.Authenticator{ toAuthenticator("noop", simpleJWTConfig()), - toAuthenticator("jwt", emptyJWTConfig()), }, }, { @@ -655,30 +665,26 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then - Expect(problems).To(HaveLen(6)) + Expect(problems).To(HaveLen(5)) Expect(problems[0].AttributePath).To(Equal(".spec.rules")) Expect(problems[0].Message).To(Equal("multiple rules defined for the same path and method")) Expect(problems[1].AttributePath).To(Equal(".spec.rules[0].accessStrategies[0].config")) Expect(problems[1].Message).To(Equal("strategy: noop does not support configuration")) - Expect(problems[2].AttributePath).To(Equal(".spec.rules[0].accessStrategies[1].config")) - Expect(problems[2].Message).To(Equal("supplied config cannot be empty")) + Expect(problems[2].AttributePath).To(Equal(".spec.rules[1].accessStrategies[0].config")) + Expect(problems[2].Message).To(Equal("strategy: anonymous does not support configuration")) - Expect(problems[3].AttributePath).To(Equal(".spec.rules[1].accessStrategies[0].config")) - Expect(problems[3].Message).To(Equal("strategy: anonymous does not support configuration")) + Expect(problems[3].AttributePath).To(Equal(".spec.rules[2].accessStrategies[0].handler")) + Expect(problems[3].Message).To(Equal("Unsupported accessStrategy: non-existing")) - Expect(problems[4].AttributePath).To(Equal(".spec.rules[2].accessStrategies[0].handler")) - Expect(problems[4].Message).To(Equal("Unsupported accessStrategy: non-existing")) - - Expect(problems[5].AttributePath).To(Equal(".spec.rules[3].accessStrategies")) - Expect(problems[5].Message).To(Equal("No accessStrategies defined")) + Expect(problems[4].AttributePath).To(Equal(".spec.rules[3].accessStrategies")) + Expect(problems[4].Message).To(Equal("No accessStrategies defined")) }) - It("Should fail for the same path and method", func() { //given input := &gatewayv1beta1.APIRule{ @@ -706,14 +712,13 @@ var _ = Describe("Validate function", func() { //when problems := (&APIRule{ DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{}) //then Expect(problems).To(HaveLen(1)) Expect(problems[0].AttributePath).To(Equal(".spec.rules")) Expect(problems[0].Message).To(Equal("multiple rules defined for the same path and method")) }) - It("Should succeed for valid input", func() { //given occupiedHost := "occupied-host" + allowlistedDomain @@ -763,8 +768,9 @@ var _ = Describe("Validate function", func() { } //when problems := (&APIRule{ + JwtValidator: jwtValidatorMock, DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{Items: []*networkingv1beta1.VirtualService{&existingVS}}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{Items: []*networkingv1beta1.VirtualService{&existingVS}}) //then Expect(problems).To(HaveLen(0)) @@ -807,8 +813,9 @@ var _ = Describe("Validate function", func() { } //when problems := (&APIRule{ + JwtValidator: jwtValidatorMock, DomainAllowList: testDomainAllowlist, - }).Validate(input, networkingv1beta1.VirtualServiceList{Items: []*networkingv1beta1.VirtualService{&existingVS}}, &config) + }).Validate(input, networkingv1beta1.VirtualServiceList{Items: []*networkingv1beta1.VirtualService{&existingVS}}) //then Expect(problems).To(HaveLen(0)) @@ -824,7 +831,7 @@ var _ = Describe("Validator for", func() { handler := &gatewayv1beta1.Handler{Name: "noop", Config: simpleJWTConfig("http://atgo.org")} //when - problems := (&noConfigAccStrValidator{}).Validate("some.attribute", handler, &config) + problems := (&noConfigAccStrValidator{}).Validate("some.attribute", handler) //then Expect(problems).To(HaveLen(1)) @@ -837,7 +844,7 @@ var _ = Describe("Validator for", func() { handler := &gatewayv1beta1.Handler{Name: "noop", Config: emptyConfig()} //when - problems := (&noConfigAccStrValidator{}).Validate("some.attribute", handler, &config) + problems := (&noConfigAccStrValidator{}).Validate("some.attribute", handler) //then Expect(problems).To(HaveLen(0)) @@ -848,246 +855,30 @@ var _ = Describe("Validator for", func() { handler := &gatewayv1beta1.Handler{Name: "noop", Config: nil} //when - problems := (&noConfigAccStrValidator{}).Validate("some.attribute", handler, &config) + problems := (&noConfigAccStrValidator{}).Validate("some.attribute", handler) //then Expect(problems).To(HaveLen(0)) }) }) - Describe("JWT access strategy", func() { - - It("Should fail with empty config", func() { - //given - handler := &gatewayv1beta1.Handler{Name: "jwt", Config: emptyJWTConfig()} - - //when - problems := (&jwtAccStrValidator{}).Validate("some.attribute", handler, &config) - - //then - Expect(problems).To(HaveLen(1)) - Expect(problems[0].AttributePath).To(Equal("some.attribute.config")) - Expect(problems[0].Message).To(Equal("supplied config cannot be empty")) - }) - - It("Should fail for config with invalid trustedIssuers and JWKSUrls", func() { - //given - handler := &gatewayv1beta1.Handler{Name: "jwt", Config: simpleJWTConfig("a t g o")} - - //when - problems := (&jwtAccStrValidator{}).Validate("some.attribute", handler, &config) - - //then - Expect(problems).To(HaveLen(2)) - Expect(problems[0].AttributePath).To(Equal("some.attribute.config.trusted_issuers[0]")) - Expect(problems[0].Message).To(ContainSubstring("value is empty or not a valid url")) - Expect(problems[1].AttributePath).To(Equal("some.attribute.config.jwks_urls[0]")) - Expect(problems[1].Message).To(ContainSubstring("value is empty or not a valid url")) - }) - - It("Should fail for config with plain HTTP JWKSUrls and trustedIssuers", func() { - //given - handler := &gatewayv1beta1.Handler{Name: "jwt", Config: testURLJWTConfig("http://issuer.test/.well-known/jwks.json", "http://issuer.test/")} - - //when - problems := (&jwtAccStrValidator{}).Validate("some.attribute", handler, &config) - - //then - Expect(problems).To(HaveLen(2)) - Expect(problems[0].AttributePath).To(Equal("some.attribute.config.trusted_issuers[0]")) - Expect(problems[0].Message).To(ContainSubstring("value is not a secured url")) - Expect(problems[1].AttributePath).To(Equal("some.attribute.config.jwks_urls[0]")) - Expect(problems[1].Message).To(ContainSubstring("value is not a secured url")) - }) - - It("Should succeed for config with file JWKSUrls and HTTPS trustedIssuers", func() { - //given - handler := &gatewayv1beta1.Handler{Name: "jwt", Config: testURLJWTConfig("file://.well-known/jwks.json", "https://issuer.test/")} - - //when - problems := (&jwtAccStrValidator{}).Validate("some.attribute", handler, &config) - - //then - Expect(problems).To(HaveLen(0)) - }) - - It("Should succeed for config with HTTPS JWKSUrls and trustedIssuers", func() { - //given - handler := &gatewayv1beta1.Handler{Name: "jwt", Config: testURLJWTConfig("https://issuer.test/.well-known/jwks.json", "https://issuer.test/")} - - //when - problems := (&jwtAccStrValidator{}).Validate("some.attribute", handler, &config) - - //then - Expect(problems).To(HaveLen(0)) - }) - - It("Should fail for invalid JSON", func() { - //given - handler := &gatewayv1beta1.Handler{Name: "jwt", Config: &runtime.RawExtension{Raw: []byte("/abc]")}} - - //when - problems := (&jwtAccStrValidator{}).Validate("some.attribute", handler, &config) - - //then - Expect(problems).To(HaveLen(1)) - Expect(problems[0].AttributePath).To(Equal("some.attribute.config")) - Expect(problems[0].Message).To(Equal("Can't read json: invalid character '/' looking for beginning of value")) - }) - - It("Should succeed with valid config", func() { - //given - handler := &gatewayv1beta1.Handler{Name: "jwt", Config: simpleJWTConfig()} - - //when - problems := (&jwtAccStrValidator{}).Validate("some.attribute", handler, &config) - - //then - Expect(problems).To(HaveLen(0)) - }) - - Context("When the jwt handler is istio", func() { - configIstioJWT := helpers.Config{JWTHandler: helpers.JWT_HANDLER_ISTIO} - - It("Should fail with empty config", func() { - //given - handler := &gatewayv1beta1.Handler{Name: "jwt", Config: emptyJWTIstioConfig()} - - //when - problems := (&jwtAccStrValidator{}).Validate("some.attribute", handler, &configIstioJWT) - - //then - Expect(problems).To(HaveLen(1)) - Expect(problems[0].AttributePath).To(Equal("some.attribute.config")) - Expect(problems[0].Message).To(Equal("supplied config cannot be empty")) - }) - - It("Should fail for config with invalid trustedIssuers and JWKSUrls", func() { - //given - handler := &gatewayv1beta1.Handler{Name: "jwt", Config: simpleJWTIstioConfig("a t g o")} - - //when - problems := (&jwtAccStrValidator{}).Validate("some.attribute", handler, &configIstioJWT) - - //then - Expect(problems).To(HaveLen(2)) - Expect(problems[0].AttributePath).To(Equal("some.attribute.config.authentications[0].issuer")) - Expect(problems[0].Message).To(ContainSubstring("value is empty or not a valid url")) - Expect(problems[1].AttributePath).To(Equal("some.attribute.config.authentications[0].jwksUri")) - Expect(problems[1].Message).To(ContainSubstring("value is empty or not a valid url")) - }) - - It("Should fail for config with plain HTTP JWKSUrls and trustedIssuers", func() { - //given - handler := &gatewayv1beta1.Handler{Name: "jwt", Config: testURLJWTIstioConfig("http://issuer.test/.well-known/jwks.json", "http://issuer.test/")} - - //when - problems := (&jwtAccStrValidator{}).Validate("some.attribute", handler, &configIstioJWT) - - //then - Expect(problems).To(HaveLen(2)) - Expect(problems[0].AttributePath).To(Equal("some.attribute.config.authentications[0].issuer")) - Expect(problems[0].Message).To(ContainSubstring("value is not a secured url")) - Expect(problems[1].AttributePath).To(Equal("some.attribute.config.authentications[0].jwksUri")) - Expect(problems[1].Message).To(ContainSubstring("value is not a secured url")) - }) - - It("Should succeed for config with file JWKSUrls and HTTPS trustedIssuers", func() { - //given - handler := &gatewayv1beta1.Handler{Name: "jwt", Config: testURLJWTIstioConfig("file://.well-known/jwks.json", "https://issuer.test/")} - - //when - problems := (&jwtAccStrValidator{}).Validate("some.attribute", handler, &configIstioJWT) - - //then - Expect(problems).To(HaveLen(0)) - }) - - It("Should succeed for config with HTTPS JWKSUrls and trustedIssuers", func() { - //given - handler := &gatewayv1beta1.Handler{Name: "jwt", Config: testURLJWTIstioConfig("https://issuer.test/.well-known/jwks.json", "https://issuer.test/")} - - //when - problems := (&jwtAccStrValidator{}).Validate("some.attribute", handler, &configIstioJWT) - - //then - Expect(problems).To(HaveLen(0)) - }) - - It("Should fail for invalid JSON", func() { - //given - handler := &gatewayv1beta1.Handler{Name: "jwt", Config: &runtime.RawExtension{Raw: []byte("/abc]")}} - - //when - problems := (&jwtAccStrValidator{}).Validate("some.attribute", handler, &configIstioJWT) - - //then - Expect(problems).To(HaveLen(1)) - Expect(problems[0].AttributePath).To(Equal("some.attribute.config")) - Expect(problems[0].Message).To(Equal("Can't read json: invalid character '/' looking for beginning of value")) - }) - }) - }) }) func emptyConfig() *runtime.RawExtension { - var emptyConfig struct{} - return getRawConfig(emptyConfig) -} - -func emptyJWTConfig() *runtime.RawExtension { - return getRawConfig( - &oryint.JWTAccStrConfig{}) -} - -func emptyJWTIstioConfig() *runtime.RawExtension { return getRawConfig( - &istioint.JwtConfig{}) + &ory.JWTAccStrConfig{}) } func simpleJWTConfig(trustedIssuers ...string) *runtime.RawExtension { return getRawConfig( - &oryint.JWTAccStrConfig{ + &ory.JWTAccStrConfig{ JWKSUrls: trustedIssuers, TrustedIssuers: trustedIssuers, RequiredScopes: []string{"atgo"}, }) } -func simpleJWTIstioConfig(trustedIssuers ...string) *runtime.RawExtension { - issuers := []istioint.JwtAuth{} - for _, issuer := range trustedIssuers { - issuers = append(issuers, istioint.JwtAuth{ - Issuer: issuer, - JwksUri: issuer, - }) - } - jwtConfig := istioint.JwtConfig{Authentications: issuers} - return getRawConfig(jwtConfig) -} - -func testURLJWTConfig(JWKSUrls string, trustedIssuers string) *runtime.RawExtension { - return getRawConfig( - &oryint.JWTAccStrConfig{ - JWKSUrls: []string{JWKSUrls}, - TrustedIssuers: []string{trustedIssuers}, - RequiredScopes: []string{"atgo"}, - }) -} - -func testURLJWTIstioConfig(JWKSUrl string, trustedIssuer string) *runtime.RawExtension { - return getRawConfig( - istioint.JwtConfig{ - Authentications: []istioint.JwtAuth{ - { - Issuer: trustedIssuer, - JwksUri: JWKSUrl, - }, - }, - }) -} - -func getRawConfig(config any) *runtime.RawExtension { +func getRawConfig(config *ory.JWTAccStrConfig) *runtime.RawExtension { bytes, err := json.Marshal(config) Expect(err).To(BeNil()) return &runtime.RawExtension{