diff --git a/docs/gateway-api-compatibility.md b/docs/gateway-api-compatibility.md index 62cd7f2b2..c20e0d02e 100644 --- a/docs/gateway-api-compatibility.md +++ b/docs/gateway-api-compatibility.md @@ -12,7 +12,7 @@ This document describes which Gateway API resources NGINX Kubernetes Gateway sup | [TLSRoute](#tlsroute) | Not supported | | [TCPRoute](#tcproute) | Not supported | | [UDPRoute](#udproute) | Not supported | -| [ReferenceGrant](#referencegrant) | Partially supported | +| [ReferenceGrant](#referencegrant) | Supported | | [Custom policies](#custom-policies) | Not supported | ## Terminology @@ -152,19 +152,18 @@ Fields: ### ReferenceGrant -> Status: Partially supported. - -NKG only supports ReferenceGrants that permit Gateways to reference Secrets. +> Status: Supported. +> Support Level: Core Fields: * `spec` * `to` * `group` - supported. - * `kind` - partially supported. Only `Secret`. + * `kind` - supports `Secret` and `Service`. * `name`- supported. * `from` * `group` - supported. - * `kind` - partially supported. Only `Gateway`. + * `kind` - supports `Gateway` and `HTTPRoute`. * `namespace`- supported. ### Custom Policies diff --git a/examples/cross-namespace-routing/README.md b/examples/cross-namespace-routing/README.md new file mode 100644 index 000000000..f2fa523bf --- /dev/null +++ b/examples/cross-namespace-routing/README.md @@ -0,0 +1,128 @@ +# Example + +In this example, we expand on the simple [cafe-example](../cafe-example) by using a ReferenceGrant to route to backends +in a different namespace from our HTTPRoutes. + +## Running the Example + +## 1. Deploy NGINX Kubernetes Gateway + +1. Follow the [installation instructions](/docs/installation.md) to deploy NGINX Gateway. + +1. Save the public IP address of NGINX Kubernetes Gateway into a shell variable: + + ``` + GW_IP=XXX.YYY.ZZZ.III + ``` + +1. Save the port of NGINX Kubernetes Gateway: + + ``` + GW_PORT= + ``` + +## 2. Deploy the Cafe Application + +1. Create the cafe namespace and cafe application: + + ``` + kubectl apply -f cafe-ns-and-app.yaml + ``` + +1. Check that the Pods are running in the `cafe` namespace: + + ``` + kubectl -n cafe get pods + NAME READY STATUS RESTARTS AGE + coffee-6f4b79b975-2sb28 1/1 Running 0 12s + tea-6fb46d899f-fm7zr 1/1 Running 0 12s + ``` + +## 3. Configure Routing + +1. Create the `Gateway`: + + ``` + kubectl apply -f gateway.yaml + ``` + +1. Create the `HTTPRoute` resources: + + ``` + kubectl apply -f cafe-routes.yaml + ``` +1. Create the `ReferenceGrant`: + + ``` + kubectl apply -f reference-grant.yaml + ``` + This ReferenceGrant allows all HTTPRoutes in the `default` namespace to reference all Services in the `cafe` + namespace. + +## 4. Test the Application + +To access the application, we will use `curl` to send requests to the `coffee` and `tea` Services. + +To get coffee: + +``` +curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee +Server address: 10.12.0.18:80 +Server name: coffee-7586895968-r26zn +``` + +To get tea: + +``` +curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/tea +Server address: 10.12.0.19:80 +Server name: tea-7cd44fcb4d-xfw2x +``` + +## 5. Remove the ReferenceGrant + +To restrict access to Services in the `cafe` Namespace, we can delete the ReferenceGrant we created in +Step 3: + +``` +kubectl delete -f reference-grant.yaml +``` + +Now, if we try to access the application over HTTP, we will get an internal server error: +``` +curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/tea + + +500 Internal Server Error + +

500 Internal Server Error

+
nginx/1.25.1
+ + +``` + +You can also check the conditions of the HTTPRoutes `coffee` and `tea` to verify that the reference is not permitted: + +``` +kubectl describe httproute coffee + +Condtions: + Message: Backend ref to Service cafe/coffee not permitted by any ReferenceGrant + Observed Generation: 1 + Reason: RefNotPermitted + Status: False + Type: ResolvedRefs + Controller Name: k8s-gateway.nginx.org/nginx-gateway-controller +``` + +``` +kubectl describe httproute tea + +Condtions: + Message: Backend ref to Service cafe/tea not permitted by any ReferenceGrant + Observed Generation: 1 + Reason: RefNotPermitted + Status: False + Type: ResolvedRefs + Controller Name: k8s-gateway.nginx.org/nginx-gateway-controller +``` diff --git a/examples/cross-namespace-routing/cafe-ns-and-app.yaml b/examples/cross-namespace-routing/cafe-ns-and-app.yaml new file mode 100644 index 000000000..d8bbec5f3 --- /dev/null +++ b/examples/cross-namespace-routing/cafe-ns-and-app.yaml @@ -0,0 +1,74 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: cafe +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: coffee + namespace: cafe +spec: + replicas: 1 + selector: + matchLabels: + app: coffee + template: + metadata: + labels: + app: coffee + spec: + containers: + - name: coffee + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: coffee + namespace: cafe +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: coffee +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tea + namespace: cafe +spec: + replicas: 1 + selector: + matchLabels: + app: tea + template: + metadata: + labels: + app: tea + spec: + containers: + - name: tea + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: tea + namespace: cafe +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: tea diff --git a/examples/cross-namespace-routing/cafe-routes.yaml b/examples/cross-namespace-routing/cafe-routes.yaml new file mode 100644 index 000000000..6f78c3680 --- /dev/null +++ b/examples/cross-namespace-routing/cafe-routes.yaml @@ -0,0 +1,39 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: HTTPRoute +metadata: + name: coffee +spec: + parentRefs: + - name: gateway + sectionName: http + hostnames: + - "cafe.example.com" + rules: + - matches: + - path: + type: PathPrefix + value: /coffee + backendRefs: + - name: coffee + namespace: cafe + port: 80 +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: HTTPRoute +metadata: + name: tea +spec: + parentRefs: + - name: gateway + sectionName: http + hostnames: + - "cafe.example.com" + rules: + - matches: + - path: + type: Exact + value: /tea + backendRefs: + - name: tea + namespace: cafe + port: 80 diff --git a/examples/cross-namespace-routing/gateway.yaml b/examples/cross-namespace-routing/gateway.yaml new file mode 100644 index 000000000..f9859bfa1 --- /dev/null +++ b/examples/cross-namespace-routing/gateway.yaml @@ -0,0 +1,13 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: gateway + labels: + domain: k8s-gateway.nginx.org +spec: + gatewayClassName: nginx + listeners: + - name: http + port: 80 + protocol: HTTP + hostname: "*.example.com" diff --git a/examples/cross-namespace-routing/reference-grant.yaml b/examples/cross-namespace-routing/reference-grant.yaml new file mode 100644 index 000000000..17e526458 --- /dev/null +++ b/examples/cross-namespace-routing/reference-grant.yaml @@ -0,0 +1,13 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: access-to-cafe-services + namespace: cafe +spec: + to: + - group: "" + kind: Service + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: default diff --git a/examples/https-termination/README.md b/examples/https-termination/README.md index 997494ab0..0fda43300 100644 --- a/examples/https-termination/README.md +++ b/examples/https-termination/README.md @@ -164,6 +164,8 @@ curl: (7) Failed to connect to cafe.example.com port 443 after 0 ms: Connection You can also check the conditions of the Gateway `https` Listener to verify the that the reference is not permitted: ``` + kubectl describe gateway gateway + Name: https Conditions: Last Transition Time: 2023-06-26T20:23:56Z diff --git a/examples/https-termination/reference-grant.yaml b/examples/https-termination/reference-grant.yaml index ac0e212f1..053bbbdcc 100644 --- a/examples/https-termination/reference-grant.yaml +++ b/examples/https-termination/reference-grant.yaml @@ -1,7 +1,7 @@ apiVersion: gateway.networking.k8s.io/v1beta1 kind: ReferenceGrant metadata: - name: allow-default-to-cafe-secret + name: access-to-cafe-secret namespace: certificate spec: to: diff --git a/internal/state/change_processor_test.go b/internal/state/change_processor_test.go index c8beaf334..2c11ab3c6 100644 --- a/internal/state/change_processor_test.go +++ b/internal/state/change_processor_test.go @@ -228,23 +228,38 @@ var _ = Describe("ChangeProcessor", func() { gcUpdated *v1beta1.GatewayClass hr1, hr1Updated, hr2 *v1beta1.HTTPRoute gw1, gw1Updated, gw2 *v1beta1.Gateway - refGrant *v1beta1.ReferenceGrant + refGrant1, refGrant2 *v1beta1.ReferenceGrant expGraph *graph.Graph expRouteHR1, expRouteHR2 *graph.Route + hr1Name, hr2Name types.NamespacedName ) BeforeAll(func() { gcUpdated = gc.DeepCopy() gcUpdated.Generation++ - hr1 = createRoute("hr-1", "gateway-1", "foo.example.com") + crossNsBackendRef := v1beta1.HTTPBackendRef{ + BackendRef: v1beta1.BackendRef{ + BackendObjectReference: v1beta1.BackendObjectReference{ + Kind: helpers.GetPointer[v1beta1.Kind]("Service"), + Name: "service", + Namespace: helpers.GetPointer[v1beta1.Namespace]("service-ns"), + Port: helpers.GetPointer[v1beta1.PortNumber](80), + }, + }, + } + + hr1 = createRoute("hr-1", "gateway-1", "foo.example.com", crossNsBackendRef) + hr1Name = types.NamespacedName{Namespace: hr1.Namespace, Name: hr1.Name} hr1Updated = hr1.DeepCopy() hr1Updated.Generation++ hr2 = createRoute("hr-2", "gateway-2", "bar.example.com") + hr2Name = types.NamespacedName{Namespace: "test", Name: "hr-2"} gw1 = createGatewayWithTLSListener("gateway-1", "cert-ns") // cert in diff namespace than gw - refGrant = &v1beta1.ReferenceGrant{ + + refGrant1 = &v1beta1.ReferenceGrant{ ObjectMeta: metav1.ObjectMeta{ Namespace: "cert-ns", Name: "ref-grant", @@ -259,8 +274,28 @@ var _ = Describe("ChangeProcessor", func() { }, To: []v1beta1.ReferenceGrantTo{ { - Group: "core", - Kind: "Secret", + Kind: "Secret", + }, + }, + }, + } + + refGrant2 = &v1beta1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "service-ns", + Name: "ref-grant", + }, + Spec: v1beta1.ReferenceGrantSpec{ + From: []v1beta1.ReferenceGrantFrom{ + { + Group: v1beta1.GroupName, + Kind: "HTTPRoute", + Namespace: "test", + }, + }, + To: []v1beta1.ReferenceGrantTo{ + { + Kind: "Service", }, }, }, @@ -291,8 +326,23 @@ var _ = Describe("ChangeProcessor", func() { Idx: 1, }, }, - Rules: []graph.Rule{{ValidMatches: true, ValidFilters: true}}, + Rules: []graph.Rule{ + { + BackendRefs: []graph.BackendRef{ + { + Weight: 1, + }, + }, + ValidMatches: true, + ValidFilters: true, + }, + }, Valid: true, + Conditions: []conditions.Condition{ + conditions.NewRouteBackendRefRefBackendNotFound( + "spec.rules[0].backendRefs[0].name: Not found: \"service\"", + ), + }, } expRouteHR2 = &graph.Route{ @@ -381,12 +431,17 @@ var _ = Describe("ChangeProcessor", func() { expGraph.Gateway.Valid = false expGraph.Gateway.Listeners = nil - hrName := types.NamespacedName{Namespace: "test", Name: "hr-1"} - expGraph.Routes[hrName].ParentRefs[0].Attachment = &graph.ParentRefAttachmentStatus{ + // no ref grant exists yet for hr1 + expGraph.Routes[hr1Name].Conditions = []conditions.Condition{ + conditions.NewRouteBackendRefRefNotPermitted( + "Backend ref to Service service-ns/service not permitted by any ReferenceGrant", + ), + } + expGraph.Routes[hr1Name].ParentRefs[0].Attachment = &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{}, FailedCondition: conditions.NewRouteInvalidGateway(), } - expGraph.Routes[hrName].ParentRefs[1].Attachment = &graph.ParentRefAttachmentStatus{ + expGraph.Routes[hr1Name].ParentRefs[1].Attachment = &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{}, FailedCondition: conditions.NewRouteInvalidGateway(), } @@ -412,8 +467,6 @@ var _ = Describe("ChangeProcessor", func() { ), } - hr1Name := types.NamespacedName{Namespace: hr1.Namespace, Name: hr1.Name} - expAttachment := &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{}, FailedCondition: conditions.NewRouteInvalidListener(), @@ -421,7 +474,14 @@ var _ = Describe("ChangeProcessor", func() { } expGraph.Gateway.Listeners["listener-80-1"].Routes[hr1Name].ParentRefs[1].Attachment = expAttachment + + // no ref grant exists yet for hr1 expGraph.Routes[hr1Name].ParentRefs[1].Attachment = expAttachment + expGraph.Routes[hr1Name].Conditions = []conditions.Condition{ + conditions.NewRouteBackendRefRefNotPermitted( + "Backend ref to Service service-ns/service not permitted by any ReferenceGrant", + ), + } changed, graphCfg := processor.Process() Expect(changed).To(BeTrue()) @@ -430,7 +490,24 @@ var _ = Describe("ChangeProcessor", func() { }) When("the ReferenceGrant allowing the Gateway to reference its Secret is upserted", func() { It("returns updated graph", func() { - processor.CaptureUpsertChange(refGrant) + processor.CaptureUpsertChange(refGrant1) + + // no ref grant exists yet for hr1 + expGraph.Routes[hr1Name].Conditions = []conditions.Condition{ + conditions.NewRouteBackendRefRefNotPermitted( + "Backend ref to Service service-ns/service not permitted by any ReferenceGrant", + ), + } + + changed, graphCfg := processor.Process() + Expect(changed).To(BeTrue()) + Expect(helpers.Diff(expGraph, graphCfg)).To(BeEmpty()) + }) + }) + + When("the ReferenceGrant allowing the hr1 to reference the Service in different ns is upserted", func() { + It("returns updated graph", func() { + processor.CaptureUpsertChange(refGrant2) changed, graphCfg := processor.Process() Expect(changed).To(BeTrue()) @@ -452,9 +529,8 @@ var _ = Describe("ChangeProcessor", func() { It("returns populated graph", func() { processor.CaptureUpsertChange(hr1Updated) - hrName := types.NamespacedName{Namespace: "test", Name: "hr-1"} - expGraph.Gateway.Listeners["listener-443-1"].Routes[hrName].Source.Generation = hr1Updated.Generation - expGraph.Gateway.Listeners["listener-80-1"].Routes[hrName].Source.Generation = hr1Updated.Generation + expGraph.Gateway.Listeners["listener-443-1"].Routes[hr1Name].Source.Generation = hr1Updated.Generation + expGraph.Gateway.Listeners["listener-80-1"].Routes[hr1Name].Source.Generation = hr1Updated.Generation changed, graphCfg := processor.Process() Expect(changed).To(BeTrue()) @@ -531,17 +607,15 @@ var _ = Describe("ChangeProcessor", func() { It("returns populated graph", func() { processor.CaptureUpsertChange(hr2) - hrName := types.NamespacedName{Namespace: "test", Name: "hr-2"} - expGraph.IgnoredGateways = map[types.NamespacedName]*v1beta1.Gateway{ {Namespace: "test", Name: "gateway-2"}: gw2, } - expGraph.Routes[hrName] = expRouteHR2 - expGraph.Routes[hrName].ParentRefs[0].Attachment = &graph.ParentRefAttachmentStatus{ + expGraph.Routes[hr2Name] = expRouteHR2 + expGraph.Routes[hr2Name].ParentRefs[0].Attachment = &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{}, FailedCondition: conditions.NewTODO("Gateway is ignored"), } - expGraph.Routes[hrName].ParentRefs[1].Attachment = &graph.ParentRefAttachmentStatus{ + expGraph.Routes[hr2Name].ParentRefs[1].Attachment = &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{}, FailedCondition: conditions.NewTODO("Gateway is ignored"), } @@ -560,8 +634,6 @@ var _ = Describe("ChangeProcessor", func() { // gateway 2 takes over; // route 1 has been replaced by route 2 - hr1Name := types.NamespacedName{Namespace: "test", Name: "hr-1"} - hr2Name := types.NamespacedName{Namespace: "test", Name: "hr-2"} expGraph.Gateway.Source = gw2 expGraph.Gateway.Listeners["listener-80-1"].Source = gw2.Spec.Listeners[0] expGraph.Gateway.Listeners["listener-443-1"].Source = gw2.Spec.Listeners[1] @@ -587,7 +659,6 @@ var _ = Describe("ChangeProcessor", func() { // gateway 2 still in charge; // no routes remain - hr1Name := types.NamespacedName{Namespace: "test", Name: "hr-1"} expGraph.Gateway.Source = gw2 expGraph.Gateway.Listeners["listener-80-1"].Source = gw2.Spec.Listeners[0] expGraph.Gateway.Listeners["listener-443-1"].Source = gw2.Spec.Listeners[1] diff --git a/internal/state/graph/backend_refs.go b/internal/state/graph/backend_refs.go index 61402604f..b5e7e3399 100644 --- a/internal/state/graph/backend_refs.go +++ b/internal/state/graph/backend_refs.go @@ -33,17 +33,22 @@ func (b BackendRef) ServicePortReference() string { func addBackendRefsToRouteRules( routes map[types.NamespacedName]*Route, + refGrantResolver *referenceGrantResolver, services map[types.NamespacedName]*v1.Service, ) { for _, r := range routes { - addBackendRefsToRules(r, services) + addBackendRefsToRules(r, refGrantResolver, services) } } // addBackendRefsToRules iterates over the rules of a route and adds a list of BackendRef to each rule. // The route is modified in place. // If a reference in a rule is invalid, the function will add a condition to the rule. -func addBackendRefsToRules(route *Route, services map[types.NamespacedName]*v1.Service) { +func addBackendRefsToRules( + route *Route, + refGrantResolver *referenceGrantResolver, + services map[types.NamespacedName]*v1.Service, +) { if !route.Valid { return } @@ -66,7 +71,7 @@ func addBackendRefsToRules(route *Route, services map[types.NamespacedName]*v1.S for refIdx, ref := range rule.BackendRefs { refPath := field.NewPath("spec").Child("rules").Index(idx).Child("backendRefs").Index(refIdx) - ref, cond := createBackendRef(ref, route.Source.Namespace, services, refPath) + ref, cond := createBackendRef(ref, route.Source.Namespace, refGrantResolver, services, refPath) backendRefs = append(backendRefs, ref) if cond != nil { @@ -81,6 +86,7 @@ func addBackendRefsToRules(route *Route, services map[types.NamespacedName]*v1.S func createBackendRef( ref v1beta1.HTTPBackendRef, sourceNamespace string, + refGrantResolver *referenceGrantResolver, services map[types.NamespacedName]*v1.Service, refPath *field.Path, ) (BackendRef, *conditions.Condition) { @@ -99,7 +105,7 @@ func createBackendRef( var backendRef BackendRef - valid, cond := validateHTTPBackendRef(ref, sourceNamespace, refPath) + valid, cond := validateHTTPBackendRef(ref, sourceNamespace, refGrantResolver, refPath) if !valid { backendRef = BackendRef{ Weight: weight, @@ -136,7 +142,12 @@ func getServiceAndPortFromRef( services map[types.NamespacedName]*v1.Service, refPath *field.Path, ) (*v1.Service, int32, error) { - svcNsName := types.NamespacedName{Name: string(ref.Name), Namespace: routeNamespace} + ns := routeNamespace + if ref.Namespace != nil { + ns = string(*ref.Namespace) + } + + svcNsName := types.NamespacedName{Name: string(ref.Name), Namespace: ns} svc, ok := services[svcNsName] if !ok { @@ -150,6 +161,7 @@ func getServiceAndPortFromRef( func validateHTTPBackendRef( ref v1beta1.HTTPBackendRef, routeNs string, + refGrantResolver *referenceGrantResolver, path *field.Path, ) (valid bool, cond conditions.Condition) { // Because all errors cause the same condition but different reasons, we return as soon as we find an error @@ -159,12 +171,13 @@ func validateHTTPBackendRef( return false, conditions.NewRouteBackendRefUnsupportedValue(valErr.Error()) } - return validateBackendRef(ref.BackendRef, routeNs, path) + return validateBackendRef(ref.BackendRef, routeNs, refGrantResolver, path) } func validateBackendRef( ref v1beta1.BackendRef, routeNs string, + refGrantResolver *referenceGrantResolver, path *field.Path, ) (valid bool, cond conditions.Condition) { // Because all errors cause same condition but different reasons, we return as soon as we find an error @@ -182,8 +195,13 @@ func validateBackendRef( // no need to validate ref.Name if ref.Namespace != nil && string(*ref.Namespace) != routeNs { - valErr := field.Invalid(path.Child("namespace"), *ref.Namespace, "cross-namespace routing is not permitted") - return false, conditions.NewRouteBackendRefRefNotPermitted(valErr.Error()) + refNsName := types.NamespacedName{Namespace: string(*ref.Namespace), Name: string(ref.Name)} + + if !refGrantResolver.refAllowed(toService(refNsName), fromHTTPRoute(routeNs)) { + msg := fmt.Sprintf("Backend ref to Service %s not permitted by any ReferenceGrant", refNsName) + + return false, conditions.NewRouteBackendRefRefNotPermitted(msg) + } } if ref.Port == nil { diff --git a/internal/state/graph/backend_refs_test.go b/internal/state/graph/backend_refs_test.go index 8169104c7..81eb66655 100644 --- a/internal/state/graph/backend_refs_test.go +++ b/internal/state/graph/backend_refs_test.go @@ -18,12 +18,12 @@ import ( func getNormalRef() v1beta1.BackendRef { return v1beta1.BackendRef{ BackendObjectReference: v1beta1.BackendObjectReference{ - Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Service")), + Kind: helpers.GetPointer[v1beta1.Kind]("Service"), Name: "service1", - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), - Port: (*v1beta1.PortNumber)(helpers.GetInt32Pointer(80)), + Namespace: helpers.GetPointer[v1beta1.Namespace]("test"), + Port: helpers.GetPointer[v1beta1.PortNumber](80), }, - Weight: helpers.GetInt32Pointer(5), + Weight: helpers.GetPointer[int32](5), } } @@ -79,8 +79,9 @@ func TestValidateHTTPBackendRef(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewGomegaWithT(t) + resolver := newReferenceGrantResolver(nil) - valid, cond := validateHTTPBackendRef(test.ref, "test", field.NewPath("test")) + valid, cond := validateHTTPBackendRef(test.ref, "test", resolver, field.NewPath("test")) g.Expect(valid).To(Equal(test.expectedValid)) g.Expect(cond).To(Equal(test.expectedCondition)) @@ -89,8 +90,30 @@ func TestValidateHTTPBackendRef(t *testing.T) { } func TestValidateBackendRef(t *testing.T) { + specificRefGrant := &v1beta1.ReferenceGrant{ + Spec: v1beta1.ReferenceGrantSpec{ + To: []v1beta1.ReferenceGrantTo{ + { + Kind: "Service", + Name: helpers.GetPointer[v1beta1.ObjectName]("service1"), + }, + }, + From: []v1beta1.ReferenceGrantFrom{ + { + Group: v1beta1.GroupName, + Kind: "HTTPRoute", + Namespace: "test", + }, + }, + }, + } + + allInNamespaceRefGrant := specificRefGrant.DeepCopy() + allInNamespaceRefGrant.Spec.To[0].Name = nil + tests := []struct { ref v1beta1.BackendRef + refGrants map[types.NamespacedName]*v1beta1.ReferenceGrant expectedCondition conditions.Condition name string expectedValid bool @@ -116,6 +139,28 @@ func TestValidateBackendRef(t *testing.T) { }), expectedValid: true, }, + { + name: "normal case with backend ref allowed by specific reference grant", + ref: getModifiedRef(func(backend v1beta1.BackendRef) v1beta1.BackendRef { + backend.Namespace = helpers.GetPointer[v1beta1.Namespace]("cross-ns") + return backend + }), + refGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ + {Namespace: "cross-ns", Name: "rg"}: specificRefGrant, + }, + expectedValid: true, + }, + { + name: "normal case with backend ref allowed by all-in-namespace reference grant", + ref: getModifiedRef(func(backend v1beta1.BackendRef) v1beta1.BackendRef { + backend.Namespace = helpers.GetPointer[v1beta1.Namespace]("cross-ns") + return backend + }), + refGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ + {Namespace: "cross-ns", Name: "rg"}: allInNamespaceRefGrant, + }, + expectedValid: true, + }, { name: "invalid group", ref: getModifiedRef(func(backend v1beta1.BackendRef) v1beta1.BackendRef { @@ -130,7 +175,7 @@ func TestValidateBackendRef(t *testing.T) { { name: "not a service kind", ref: getModifiedRef(func(backend v1beta1.BackendRef) v1beta1.BackendRef { - backend.Kind = (*v1beta1.Kind)(helpers.GetStringPointer("NotService")) + backend.Kind = helpers.GetPointer[v1beta1.Kind]("NotService") return backend }), expectedValid: false, @@ -139,14 +184,14 @@ func TestValidateBackendRef(t *testing.T) { ), }, { - name: "invalid namespace", + name: "backend ref not allowed by reference grant", ref: getModifiedRef(func(backend v1beta1.BackendRef) v1beta1.BackendRef { - backend.Namespace = (*v1beta1.Namespace)(helpers.GetStringPointer("invalid")) + backend.Namespace = helpers.GetPointer[v1beta1.Namespace]("invalid") return backend }), expectedValid: false, expectedCondition: conditions.NewRouteBackendRefRefNotPermitted( - `test.namespace: Invalid value: "invalid": cross-namespace routing is not permitted`, + "Backend ref to Service invalid/service1 not permitted by any ReferenceGrant", ), }, { @@ -166,7 +211,8 @@ func TestValidateBackendRef(t *testing.T) { t.Run(test.name, func(t *testing.T) { g := NewGomegaWithT(t) - valid, cond := validateBackendRef(test.ref, "test", field.NewPath("test")) + resolver := newReferenceGrantResolver(test.refGrants) + valid, cond := validateBackendRef(test.ref, "test", resolver, field.NewPath("test")) g.Expect(valid).To(Equal(test.expectedValid)) g.Expect(cond).To(Equal(test.expectedCondition)) @@ -442,8 +488,8 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewGomegaWithT(t) - - addBackendRefsToRules(test.route, services) + resolver := newReferenceGrantResolver(nil) + addBackendRefsToRules(test.route, resolver, services) var actual []BackendRef if test.route.Rules != nil { @@ -571,7 +617,8 @@ func TestCreateBackend(t *testing.T) { t.Run(test.name, func(t *testing.T) { g := NewGomegaWithT(t) - backend, cond := createBackendRef(test.ref, sourceNamespace, services, refPath) + resolver := newReferenceGrantResolver(nil) + backend, cond := createBackendRef(test.ref, sourceNamespace, resolver, services, refPath) g.Expect(helpers.Diff(test.expectedBackend, backend)).To(BeEmpty()) g.Expect(cond).To(Equal(test.expectedCondition)) diff --git a/internal/state/graph/gateway.go b/internal/state/graph/gateway.go index 26e60077f..3d5f86ed0 100644 --- a/internal/state/graph/gateway.go +++ b/internal/state/graph/gateway.go @@ -94,7 +94,7 @@ func buildGateway( gw *v1beta1.Gateway, secretMemoryMgr secrets.SecretDiskMemoryManager, gc *GatewayClass, - refGrants map[types.NamespacedName]*v1beta1.ReferenceGrant, + refGrantResolver *referenceGrantResolver, ) *Gateway { if gw == nil { return nil @@ -112,7 +112,7 @@ func buildGateway( return &Gateway{ Source: gw, - Listeners: buildListeners(gw, secretMemoryMgr, refGrants), + Listeners: buildListeners(gw, secretMemoryMgr, refGrantResolver), Valid: true, } } diff --git a/internal/state/graph/gateway_listener.go b/internal/state/graph/gateway_listener.go index 87512dd72..b76b0d7c2 100644 --- a/internal/state/graph/gateway_listener.go +++ b/internal/state/graph/gateway_listener.go @@ -36,11 +36,11 @@ type Listener struct { func buildListeners( gw *v1beta1.Gateway, secretMemoryMgr secrets.SecretDiskMemoryManager, - refGrants map[types.NamespacedName]*v1beta1.ReferenceGrant, + refGrantResolver *referenceGrantResolver, ) map[string]*Listener { listeners := make(map[string]*Listener) - listenerFactory := newListenerConfiguratorFactory(gw, secretMemoryMgr, refGrants) + listenerFactory := newListenerConfiguratorFactory(gw, secretMemoryMgr, refGrantResolver) for _, gl := range gw.Spec.Listeners { configurator := listenerFactory.getConfiguratorForListener(gl) @@ -68,7 +68,7 @@ func (f *listenerConfiguratorFactory) getConfiguratorForListener(l v1beta1.Liste func newListenerConfiguratorFactory( gw *v1beta1.Gateway, secretMemoryMgr secrets.SecretDiskMemoryManager, - refGrants map[types.NamespacedName]*v1beta1.ReferenceGrant, + refGrantResolver *referenceGrantResolver, ) *listenerConfiguratorFactory { sharedPortConflictResolver := createPortConflictResolver() @@ -107,7 +107,7 @@ func newListenerConfiguratorFactory( sharedPortConflictResolver, }, externalReferenceResolvers: []listenerExternalReferenceResolver{ - createExternalReferencesForTLSSecretsResolver(gw.Namespace, secretMemoryMgr, refGrants), + createExternalReferencesForTLSSecretsResolver(gw.Namespace, secretMemoryMgr, refGrantResolver), }, }, } @@ -376,7 +376,7 @@ func createPortConflictResolver() listenerConflictResolver { func createExternalReferencesForTLSSecretsResolver( gwNs string, secretMemoryMgr secrets.SecretDiskMemoryManager, - refGrants map[types.NamespacedName]*v1beta1.ReferenceGrant, + refGrantResolver *referenceGrantResolver, ) listenerExternalReferenceResolver { return func(l *Listener) { certRef := l.Source.TLS.CertificateRefs[0] @@ -392,12 +392,8 @@ func createExternalReferencesForTLSSecretsResolver( } if certRefNs != gwNs { - if !refGrantAllowsGatewayToSecret(refGrants, gwNs, certRefNsName) { - msg := fmt.Sprintf( - "Certificate ref to secret %s/%s not permitted by any ReferenceGrant", - certRefNs, - certRef.Name, - ) + if !refGrantResolver.refAllowed(toSecret(certRefNsName), fromGateway(gwNs)) { + msg := fmt.Sprintf("Certificate ref to secret %s not permitted by any ReferenceGrant", certRefNsName) l.Conditions = append(l.Conditions, conditions.NewListenerRefNotPermitted(msg)...) l.Valid = false diff --git a/internal/state/graph/gateway_test.go b/internal/state/graph/gateway_test.go index b4de39b35..9116c1ca6 100644 --- a/internal/state/graph/gateway_test.go +++ b/internal/state/graph/gateway_test.go @@ -744,7 +744,8 @@ func TestBuildGateway(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewGomegaWithT(t) - result := buildGateway(test.gateway, secretMemoryMgr, test.gatewayClass, test.refGrants) + resolver := newReferenceGrantResolver(test.refGrants) + result := buildGateway(test.gateway, secretMemoryMgr, test.gatewayClass, resolver) g.Expect(helpers.Diff(test.expected, result)).To(BeEmpty()) }) } diff --git a/internal/state/graph/graph.go b/internal/state/graph/graph.go index c73c2a345..3c8a4297e 100644 --- a/internal/state/graph/graph.go +++ b/internal/state/graph/graph.go @@ -53,11 +53,13 @@ func BuildGraph( gc := buildGatewayClass(processedGwClasses.Winner) processedGws := processGateways(state.Gateways, gcName) - gw := buildGateway(processedGws.Winner, secretMemoryMgr, gc, state.ReferenceGrants) + + refGrantResolver := newReferenceGrantResolver(state.ReferenceGrants) + gw := buildGateway(processedGws.Winner, secretMemoryMgr, gc, refGrantResolver) routes := buildRoutesForGateways(validators.HTTPFieldsValidator, state.HTTPRoutes, processedGws.GetAllNsNames()) bindRoutesToListeners(routes, gw, state.Namespaces) - addBackendRefsToRouteRules(routes, state.Services) + addBackendRefsToRouteRules(routes, refGrantResolver, state.Services) g := &Graph{ GatewayClass: gc, diff --git a/internal/state/graph/graph_test.go b/internal/state/graph/graph_test.go index f4d2a4285..68c193842 100644 --- a/internal/state/graph/graph_test.go +++ b/internal/state/graph/graph_test.go @@ -67,7 +67,7 @@ func TestBuildGraph(t *testing.T) { BackendObjectReference: v1beta1.BackendObjectReference{ Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Service")), Name: "foo", - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("service")), Port: (*v1beta1.PortNumber)(helpers.GetInt32Pointer(80)), }, }, @@ -83,7 +83,7 @@ func TestBuildGraph(t *testing.T) { hr2 := createRoute("hr-2", "wrong-gateway", "listener-80-1") hr3 := createRoute("hr-3", "gateway-1", "listener-443-1") // https listener; should not conflict with hr1 - fooSvc := &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test"}} + fooSvc := &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "service"}} hr1Refs := []BackendRef{ { @@ -143,11 +143,11 @@ func TestBuildGraph(t *testing.T) { gw1 := createGateway("gateway-1") gw2 := createGateway("gateway-2") - svc := &v1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "foo"}} + svc := &v1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: "service", Name: "foo"}} - rg := &v1beta1.ReferenceGrant{ + rgSecret := &v1beta1.ReferenceGrant{ ObjectMeta: metav1.ObjectMeta{ - Name: "rg", + Name: "rg-secret", Namespace: "certificate", }, Spec: v1beta1.ReferenceGrantSpec{ @@ -166,6 +166,27 @@ func TestBuildGraph(t *testing.T) { }, } + rgService := &v1beta1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rg-service", + Namespace: "service", + }, + Spec: v1beta1.ReferenceGrantSpec{ + From: []v1beta1.ReferenceGrantFrom{ + { + Group: v1beta1.GroupName, + Kind: "HTTPRoute", + Namespace: "test", + }, + }, + To: []v1beta1.ReferenceGrantTo{ + { + Kind: "Service", + }, + }, + }, + } + createStateWithGatewayClass := func(gc *v1beta1.GatewayClass) ClusterState { return ClusterState{ GatewayClasses: map[types.NamespacedName]*v1beta1.GatewayClass{ @@ -184,7 +205,8 @@ func TestBuildGraph(t *testing.T) { client.ObjectKeyFromObject(svc): svc, }, ReferenceGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ - client.ObjectKeyFromObject(rg): rg, + client.ObjectKeyFromObject(rgSecret): rgSecret, + client.ObjectKeyFromObject(rgService): rgService, }, } } diff --git a/internal/state/graph/reference_grant.go b/internal/state/graph/reference_grant.go index 549d6720f..7afb7e67b 100644 --- a/internal/state/graph/reference_grant.go +++ b/internal/state/graph/reference_grant.go @@ -5,59 +5,132 @@ import ( "sigs.k8s.io/gateway-api/apis/v1beta1" ) -func refGrantAllowsGatewayToSecret( - refGrants map[types.NamespacedName]*v1beta1.ReferenceGrant, - gwNs string, - secretNsName types.NamespacedName, -) bool { - for nsname, grant := range refGrants { - if nsname.Namespace != secretNsName.Namespace { - continue - } +// referenceGrantResolver resolves references from one resource to another. +type referenceGrantResolver struct { + allowed map[allowedReference]struct{} +} - if fromIncludesGatewayNs(grant.Spec.From, gwNs) && toIncludesSecret(grant.Spec.To, secretNsName.Name) { - return true - } - } +// allowedReference represents an allowed reference from one resource to another. +type allowedReference struct { + to toResource + from fromResource +} - return false +// toResource represents the resource that the ReferenceGrant is granting access to. +// Maps to the v1beta1.ReferenceGrantTo. +type toResource struct { + // if group is core, this should be set to "". + group string + kind string + name string + namespace string } -func fromIncludesGatewayNs(fromList []v1beta1.ReferenceGrantFrom, gwNs string) bool { - for _, from := range fromList { - if from.Group != v1beta1.GroupName { - continue - } +// fromResource represents the resource that the ReferenceGrant is granting access from. +// Maps to the v1beta1.ReferenceGrantFrom. +type fromResource struct { + group string + kind string + namespace string +} - if from.Kind != "Gateway" { - continue - } +// The following functions are helper functions that create toResources and fromResources for the ReferenceGrant +// resources that we support. Use these functions when calling refAllowed instead of creating your own toResource and +// fromResource. - if string(from.Namespace) != gwNs { - continue - } +func toSecret(nsname types.NamespacedName) toResource { + return toResource{ + kind: "Secret", + name: nsname.Name, + namespace: nsname.Namespace, + } +} - return true +func toService(nsname types.NamespacedName) toResource { + return toResource{ + kind: "Service", + name: nsname.Name, + namespace: nsname.Namespace, } +} - return false +func fromGateway(namespace string) fromResource { + return fromResource{ + group: v1beta1.GroupName, + kind: "Gateway", + namespace: namespace, + } } -func toIncludesSecret(toList []v1beta1.ReferenceGrantTo, secretName string) bool { - for _, to := range toList { - if to.Group != "" && to.Group != "core" { - continue - } +func fromHTTPRoute(namespace string) fromResource { + return fromResource{ + group: v1beta1.GroupName, + kind: "HTTPRoute", + namespace: namespace, + } +} - if to.Kind != "Secret" { - continue - } +// newReferenceGrantResolver creates a new referenceGrantResolver. +func newReferenceGrantResolver(refGrants map[types.NamespacedName]*v1beta1.ReferenceGrant) *referenceGrantResolver { + allowed := make(map[allowedReference]struct{}) + + for nsname, grant := range refGrants { + for _, to := range grant.Spec.To { + for _, from := range grant.Spec.From { - if to.Name != nil && string(*to.Name) != secretName { - continue + toName := "" + if to.Name != nil { + toName = string(*to.Name) + } + + toGroup := string(to.Group) + if toGroup == "core" { + toGroup = "" + } + + ar := allowedReference{ + to: toResource{ + group: toGroup, + kind: string(to.Kind), + name: toName, + namespace: nsname.Namespace, + }, + from: fromResource{ + group: string(from.Group), + kind: string(from.Kind), + namespace: string(from.Namespace), + }, + } + + allowed[ar] = struct{}{} + } } + } - return true + return &referenceGrantResolver{allowed: allowed} +} + +// refAllowed returns whether the reference from the fromResource to the toResource is allowed by a ReferenceGrant. +func (r *referenceGrantResolver) refAllowed(to toResource, from fromResource) bool { + specificKey := allowedReference{ + to: to, + from: from, + } + + // omit name field to check for ReferenceGrants that allow access to all resources + // of the particular kind in the namespace + allInNamespaceKey := allowedReference{ + to: toResource{ + kind: to.kind, + namespace: to.namespace, + }, + from: from, + } + + for _, key := range []allowedReference{specificKey, allInNamespaceKey} { + if _, ok := r.allowed[key]; ok { + return true + } } return false diff --git a/internal/state/graph/reference_grant_test.go b/internal/state/graph/reference_grant_test.go index 320c4206e..3dfe4547e 100644 --- a/internal/state/graph/reference_grant_test.go +++ b/internal/state/graph/reference_grant_test.go @@ -4,23 +4,43 @@ import ( "testing" . "github.com/onsi/gomega" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers" ) -func TestRefGrantAllowsGatewayToSecret(t *testing.T) { +func TestReferenceGrantResolver(t *testing.T) { gwNs := "gw-ns" secretNsName := types.NamespacedName{Namespace: "test", Name: "certificate"} - getNormalRefGrant := func() *v1beta1.ReferenceGrant { - return &v1beta1.ReferenceGrant{ - ObjectMeta: metav1.ObjectMeta{ - Name: "rg", - Namespace: "test", + refGrants := map[types.NamespacedName]*v1beta1.ReferenceGrant{ + {Namespace: "test", Name: "valid"}: { + Spec: v1beta1.ReferenceGrantSpec{ + From: []v1beta1.ReferenceGrantFrom{ + { + Group: v1beta1.GroupName, + Kind: "Gateway", + Namespace: v1beta1.Namespace(gwNs), + }, + }, + To: []v1beta1.ReferenceGrantTo{ + { + Kind: "Secret", + Name: helpers.GetPointer(v1beta1.ObjectName("wrong-name1")), + }, + { + Kind: "Secret", + Name: helpers.GetPointer(v1beta1.ObjectName("wrong-name2")), + }, + { + Kind: "Secret", + Name: helpers.GetPointer(v1beta1.ObjectName(secretNsName.Name)), // matches + }, + }, }, + }, + {Namespace: "explicit-core-group", Name: "valid"}: { Spec: v1beta1.ReferenceGrantSpec{ From: []v1beta1.ReferenceGrantFrom{ { @@ -37,165 +57,164 @@ func TestRefGrantAllowsGatewayToSecret(t *testing.T) { }, }, }, - } + }, + {Namespace: "all-in-namespace", Name: "valid"}: { + Spec: v1beta1.ReferenceGrantSpec{ + To: []v1beta1.ReferenceGrantTo{ + { + Kind: "Secret", + }, + }, + From: []v1beta1.ReferenceGrantFrom{ + { + Group: v1beta1.GroupName, + Kind: "Gateway", + Namespace: "wrong-ns1", + }, + { + Group: v1beta1.GroupName, + Kind: "Gateway", + Namespace: "wrong-ns2", + }, + { + Group: v1beta1.GroupName, + Kind: "Gateway", + Namespace: v1beta1.Namespace(gwNs), + }, + }, + }, + }, } - createModifiedRefGrant := func(mod func(rg *v1beta1.ReferenceGrant)) *v1beta1.ReferenceGrant { - rg := getNormalRefGrant() - mod(rg) - return rg - } + normalTo := toResource{kind: "Secret", name: secretNsName.Name, namespace: secretNsName.Namespace} + normalFrom := fromResource{group: v1beta1.GroupName, kind: "Gateway", namespace: gwNs} tests := []struct { - refGrants map[types.NamespacedName]*v1beta1.ReferenceGrant - msg string - allowed bool + to toResource + from fromResource + msg string + allowed bool }{ { - msg: "allowed; specific ref grant exists", - refGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ - {Namespace: "wrong-ns", Name: "rg"}: createModifiedRefGrant(func(rg *v1beta1.ReferenceGrant) { - rg.Namespace = "wrong-ns" - }), - {Namespace: "test", Name: "wrong-to-kind"}: createModifiedRefGrant(func(rg *v1beta1.ReferenceGrant) { - rg.Spec.To[0].Kind = "WrongKind" - rg.Name = "wrong-to-kind" - }), - {Namespace: "test", Name: "rg"}: getNormalRefGrant(), - }, - allowed: true, - }, - { - msg: "allowed; all-namespace ref grant exists", - refGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ - {Namespace: "test", Name: "rg"}: createModifiedRefGrant(func(rg *v1beta1.ReferenceGrant) { - rg.Spec.To[0].Name = nil - }), - }, - allowed: true, - }, - { - msg: "allowed; implicit 'to' Group", - refGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ - {Namespace: "test", Name: "rg"}: createModifiedRefGrant(func(rg *v1beta1.ReferenceGrant) { - rg.Spec.To[0].Group = "" - }), - }, - allowed: true, - }, - { - msg: "allowed; one matching 'to' ref", - refGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ - {Namespace: "test", Name: "rg"}: createModifiedRefGrant(func(rg *v1beta1.ReferenceGrant) { - rg.Spec.To = []v1beta1.ReferenceGrantTo{ - { - Group: "wrong.group", - }, - { - Kind: "WrongKind", - }, - { - Group: "core", - Kind: "Secret", - Name: helpers.GetPointer(v1beta1.ObjectName(secretNsName.Name)), - }, - } - }), - }, - allowed: true, + msg: "wrong 'to' kind", + to: toResource{kind: "WrongKind", name: secretNsName.Name, namespace: secretNsName.Namespace}, + from: normalFrom, + allowed: false, }, { - msg: "allowed; one matching 'from' ref", - refGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ - {Namespace: "test", Name: "rg"}: createModifiedRefGrant(func(rg *v1beta1.ReferenceGrant) { - rg.Spec.From = []v1beta1.ReferenceGrantFrom{ - { - Group: "wrong.group", - }, - { - Kind: "WrongKind", - }, - { - Group: "gateway.networking.k8s.io", - Kind: "Gateway", - Namespace: v1beta1.Namespace(gwNs), - }, - } - }), + msg: "wrong 'to' group", + to: toResource{ + group: "wrong.group", + kind: "Secret", + name: secretNsName.Name, + namespace: secretNsName.Namespace, }, - allowed: true, + from: normalFrom, + allowed: false, }, { - msg: "not allowed; no ref group in secret namespace", - refGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ - {Namespace: "wrong-ns", Name: "rg"}: createModifiedRefGrant(func(rg *v1beta1.ReferenceGrant) { - rg.Namespace = "wrong-ns" - }), - }, + msg: "wrong 'to' name", + to: toResource{kind: "Secret", name: "wrong-name", namespace: secretNsName.Namespace}, + from: normalFrom, allowed: false, }, { - msg: "not allowed; no ref group with the right 'from' Group", - refGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ - {Namespace: "test", Name: "rg"}: createModifiedRefGrant(func(rg *v1beta1.ReferenceGrant) { - rg.Spec.From[0].Group = "wrong.group" - }), - }, + msg: "wrong 'from' kind", + to: normalTo, + from: fromResource{group: v1beta1.GroupName, kind: "WrongKind", namespace: gwNs}, allowed: false, }, { - msg: "not allowed; no ref group with the right 'from' Kind", - refGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ - {Namespace: "test", Name: "rg"}: createModifiedRefGrant(func(rg *v1beta1.ReferenceGrant) { - rg.Spec.From[0].Kind = "WrongKind" - }), - }, + msg: "wrong 'from' group", + to: normalTo, + from: fromResource{group: "wrong.group", kind: "Gateway", namespace: gwNs}, allowed: false, }, { - msg: "not allowed; no ref group with the right 'from' Namespace", - refGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ - {Namespace: "test", Name: "rg"}: createModifiedRefGrant(func(rg *v1beta1.ReferenceGrant) { - rg.Spec.From[0].Namespace = "wrong-ns" - }), - }, + msg: "wrong 'from' namespace", + to: normalTo, + from: fromResource{group: v1beta1.GroupName, kind: "Gateway", namespace: "wrong-ns"}, allowed: false, }, { - msg: "not allowed; no ref group with the right 'to' Group", - refGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ - {Namespace: "test", Name: "rg"}: createModifiedRefGrant(func(rg *v1beta1.ReferenceGrant) { - rg.Spec.To[0].Group = "wrong.group" - }), - }, - allowed: false, + msg: "allowed; matches specific reference grant", + to: normalTo, + from: normalFrom, + allowed: true, }, { - msg: "not allowed; no ref group with the right 'to' Kind", - refGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ - {Namespace: "test", Name: "rg"}: createModifiedRefGrant(func(rg *v1beta1.ReferenceGrant) { - rg.Spec.To[0].Kind = "WrongKind" - }), - }, - allowed: false, + msg: "allowed; matches all-in-namespace reference grant", + to: toResource{kind: "Secret", name: secretNsName.Name, namespace: "all-in-namespace"}, + from: normalFrom, + allowed: true, }, { - msg: "not allowed; no ref group with the right 'to' Name", - refGrants: map[types.NamespacedName]*v1beta1.ReferenceGrant{ - {Namespace: "test", Name: "rg"}: createModifiedRefGrant(func(rg *v1beta1.ReferenceGrant) { - rg.Spec.To[0].Name = helpers.GetPointer(v1beta1.ObjectName("wrong-name")) - }), - }, - allowed: false, + msg: "allowed; matches specific reference grant with explicit 'core' group name", + to: toResource{kind: "Secret", name: secretNsName.Name, namespace: "explicit-core-group"}, + from: normalFrom, + allowed: true, }, } + resolver := newReferenceGrantResolver(refGrants) + for _, test := range tests { t.Run(test.msg, func(t *testing.T) { g := NewGomegaWithT(t) - allowed := refGrantAllowsGatewayToSecret(test.refGrants, gwNs, secretNsName) - g.Expect(allowed).To(Equal(test.allowed)) + g.Expect(resolver.refAllowed(test.to, test.from)).To(Equal(test.allowed)) }) } } + +func TestToSecret(t *testing.T) { + ref := toSecret(types.NamespacedName{Namespace: "ns", Name: "secret"}) + + exp := toResource{ + kind: "Secret", + namespace: "ns", + name: "secret", + } + + g := NewGomegaWithT(t) + g.Expect(ref).To(Equal(exp)) +} + +func TestToService(t *testing.T) { + ref := toService(types.NamespacedName{Namespace: "ns", Name: "service"}) + + exp := toResource{ + kind: "Service", + namespace: "ns", + name: "service", + } + + g := NewGomegaWithT(t) + g.Expect(ref).To(Equal(exp)) +} + +func TestFromGateway(t *testing.T) { + ref := fromGateway("ns") + + exp := fromResource{ + group: v1beta1.GroupName, + kind: "Gateway", + namespace: "ns", + } + + g := NewGomegaWithT(t) + g.Expect(ref).To(Equal(exp)) +} + +func TestFromHTTPRoute(t *testing.T) { + ref := fromHTTPRoute("ns") + + exp := fromResource{ + group: v1beta1.GroupName, + kind: "HTTPRoute", + namespace: "ns", + } + + g := NewGomegaWithT(t) + g.Expect(ref).To(Equal(exp)) +}