From 86552d6421bd0087a155a9036b6db630869494c6 Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Mon, 27 Apr 2026 11:13:27 +0200 Subject: [PATCH 01/16] feat(rover-ctl): implement PatchAuthentication to map clientAuthMethod values --- rover-ctl/pkg/handlers/v0/rover.go | 56 +++++++++++++++ rover-ctl/pkg/handlers/v0/rover_test.go | 96 +++++++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/rover-ctl/pkg/handlers/v0/rover.go b/rover-ctl/pkg/handlers/v0/rover.go index 65ade9335..bdf14d006 100644 --- a/rover-ctl/pkg/handlers/v0/rover.go +++ b/rover-ctl/pkg/handlers/v0/rover.go @@ -9,6 +9,7 @@ import ( "encoding/json" "maps" "net/http" + "strings" "github.com/pkg/errors" "github.com/telekom/controlplane/rover-ctl/pkg/handlers/common" @@ -67,10 +68,65 @@ func PatchRoverRequest(ctx context.Context, obj types.Object) error { } } + PatchAuthentication(spec) + obj.SetContent(spec) return nil } +// clientAuthMethodMapping maps rover-ctl YAML values to rover-server API enum values. +var clientAuthMethodMapping = map[string]string{ + "basic": "BASIC", + "body": "POST", // Configures client authentication method, according to RFC 6749 +} + +// PatchAuthentication maps spec.authentication.m2m.clientAuthMethod from the rover-ctl +// YAML format (basic/body) to the rover-server API format (BASIC/POST) and restructures +// it into spec.authentication.clientAuthMethod. +func PatchAuthentication(spec map[string]any) { + auth, exists := spec["authentication"] + if !exists { + return + } + authMap, ok := auth.(map[string]any) + if !ok { + delete(spec, "authentication") + return + } + + m2m, exists := authMap["m2m"] + if !exists { + delete(spec, "authentication") + return + } + m2mMap, ok := m2m.(map[string]any) + if !ok { + delete(spec, "authentication") + return + } + + clientAuthMethod, exists := m2mMap["clientAuthMethod"] + if !exists { + delete(spec, "authentication") + return + } + clientAuthMethodStr, ok := clientAuthMethod.(string) + if !ok { + delete(spec, "authentication") + return + } + + apiValue, valid := clientAuthMethodMapping[strings.ToLower(clientAuthMethodStr)] + if !valid { + delete(spec, "authentication") + return + } + + spec["authentication"] = map[string]any{ + "clientAuthMethod": apiValue, + } +} + func PatchExposures(exposures []any) []map[string]any { if len(exposures) == 0 { return nil diff --git a/rover-ctl/pkg/handlers/v0/rover_test.go b/rover-ctl/pkg/handlers/v0/rover_test.go index 0756558f4..eb654d5f4 100644 --- a/rover-ctl/pkg/handlers/v0/rover_test.go +++ b/rover-ctl/pkg/handlers/v0/rover_test.go @@ -381,6 +381,102 @@ var _ = Describe("Rover Handler", func() { }) }) + Describe("PatchAuthentication", func() { + It("should map 'basic' to 'BASIC' in authentication.clientAuthMethod", func() { + obj := &types.UnstructuredObject{ + Content: map[string]any{ + "spec": map[string]any{ + "authentication": map[string]any{ + "m2m": map[string]any{ + "clientAuthMethod": "basic", + }, + }, + }, + }, + } + + err := v0.PatchRoverRequest(context.Background(), obj) + Expect(err).NotTo(HaveOccurred()) + + content := obj.GetContent() + auth, ok := content["authentication"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(auth["clientAuthMethod"]).To(Equal("BASIC")) + }) + + It("should map 'body' to 'POST' in authentication.clientAuthMethod", func() { + obj := &types.UnstructuredObject{ + Content: map[string]any{ + "spec": map[string]any{ + "authentication": map[string]any{ + "m2m": map[string]any{ + "clientAuthMethod": "body", + }, + }, + }, + }, + } + + err := v0.PatchRoverRequest(context.Background(), obj) + Expect(err).NotTo(HaveOccurred()) + + content := obj.GetContent() + auth, ok := content["authentication"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(auth["clientAuthMethod"]).To(Equal("POST")) + }) + + It("should not add authentication when it is missing", func() { + obj := &types.UnstructuredObject{ + Content: map[string]any{ + "spec": map[string]any{}, + }, + } + + err := v0.PatchRoverRequest(context.Background(), obj) + Expect(err).NotTo(HaveOccurred()) + + content := obj.GetContent() + Expect(content).NotTo(HaveKey("authentication")) + }) + + It("should drop authentication when clientAuthMethod has invalid value", func() { + obj := &types.UnstructuredObject{ + Content: map[string]any{ + "spec": map[string]any{ + "authentication": map[string]any{ + "m2m": map[string]any{ + "clientAuthMethod": "invalid", + }, + }, + }, + }, + } + + err := v0.PatchRoverRequest(context.Background(), obj) + Expect(err).NotTo(HaveOccurred()) + + content := obj.GetContent() + Expect(content).NotTo(HaveKey("authentication")) + }) + + It("should drop authentication when authentication format is invalid", func() { + obj := &types.UnstructuredObject{ + Content: map[string]any{ + "spec": map[string]any{ + "authentication": "not a map", + }, + }, + } + + err := v0.PatchRoverRequest(context.Background(), obj) + Expect(err).NotTo(HaveOccurred()) + + content := obj.GetContent() + Expect(content).NotTo(HaveKey("authentication")) + }) + }) + Describe("ResetSecret", func() { It("should send a reset secret request and return new credentials", func() { // Configure mock to return successful response From 82994b50690147a7d6f8368b7ea4604f33ab1411 Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Mon, 27 Apr 2026 11:32:16 +0200 Subject: [PATCH 02/16] test(rover-server): add tests to verify ctl output --- .../__snapshots__/suite_controller_test.snap | 76 +++++++++++++++++++ .../internal/controller/rover_test.go | 26 +++++++ 2 files changed, 102 insertions(+) diff --git a/rover-server/internal/controller/__snapshots__/suite_controller_test.snap b/rover-server/internal/controller/__snapshots__/suite_controller_test.snap index 28f8e1d44..8f2e24c39 100755 --- a/rover-server/internal/controller/__snapshots__/suite_controller_test.snap +++ b/rover-server/internal/controller/__snapshots__/suite_controller_test.snap @@ -1035,3 +1035,79 @@ "team": "hyperion" } --- + +[Rover Controller Update rover resource should accept clientAuthMethod BASIC as produced by rover-ctl - 1] +{ + "exposures": [ + { + "approval": "SIMPLE", + "basePath": "/eni/distr/v1", + "type": "api", + "upstream": "https://httpbin.org/anything", + "visibility": "WORLD" + } + ], + "id": "eni--hyperion--rover-local-sub", + "name": "rover-local-sub", + "status": { + "errors": [ + { + "cause": "NoApproval", + "message": "Approval is either rejected or suspended" + } + ], + "processingState": "done", + "state": "blocked", + "warnings": [ + { + "message": "Atleast one sub-resource is being processed" + } + ] + }, + "subscriptions": [ + { + "basePath": "/eni/distr/v1", + "type": "api" + } + ], + "zone": "dataplane1" +} +--- + +[Rover Controller Update rover resource should accept clientAuthMethod POST as produced by rover-ctl - 1] +{ + "exposures": [ + { + "approval": "SIMPLE", + "basePath": "/eni/distr/v1", + "type": "api", + "upstream": "https://httpbin.org/anything", + "visibility": "WORLD" + } + ], + "id": "eni--hyperion--rover-local-sub", + "name": "rover-local-sub", + "status": { + "errors": [ + { + "cause": "NoApproval", + "message": "Approval is either rejected or suspended" + } + ], + "processingState": "done", + "state": "blocked", + "warnings": [ + { + "message": "Atleast one sub-resource is being processed" + } + ] + }, + "subscriptions": [ + { + "basePath": "/eni/distr/v1", + "type": "api" + } + ], + "zone": "dataplane1" +} +--- diff --git a/rover-server/internal/controller/rover_test.go b/rover-server/internal/controller/rover_test.go index b7119c883..e8bdc2281 100644 --- a/rover-server/internal/controller/rover_test.go +++ b/rover-server/internal/controller/rover_test.go @@ -221,6 +221,32 @@ var _ = Describe("Rover Controller", func() { responseGroup, err := ExecuteRequest(req, groupToken) ExpectStatusWithBody(responseGroup, err, http.StatusForbidden, "application/problem+json") }) + + It("should accept clientAuthMethod BASIC as produced by rover-ctl", func() { + body := api.RoverUpdateRequest{ + Zone: "dataplane1", + Authentication: api.Authentication{ + ClientAuthMethod: api.BASIC, + }, + } + jsonBody, _ := json.Marshal(body) + req := httptest.NewRequest(http.MethodPut, "/rovers/eni--hyperion--rover-local-sub", bytes.NewReader(jsonBody)) + responseGroup, err := ExecuteRequest(req, groupToken) + ExpectStatusWithBody(responseGroup, err, http.StatusAccepted, "application/json") + }) + + It("should accept clientAuthMethod POST as produced by rover-ctl", func() { + body := api.RoverUpdateRequest{ + Zone: "dataplane1", + Authentication: api.Authentication{ + ClientAuthMethod: api.POST, + }, + } + jsonBody, _ := json.Marshal(body) + req := httptest.NewRequest(http.MethodPut, "/rovers/eni--hyperion--rover-local-sub", bytes.NewReader(jsonBody)) + responseGroup, err := ExecuteRequest(req, groupToken) + ExpectStatusWithBody(responseGroup, err, http.StatusAccepted, "application/json") + }) }) Context("Reset rover secret", func() { From 55eeea7034a16b5e10f9de4e3e173fe5c6b0510c Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Mon, 27 Apr 2026 11:39:19 +0200 Subject: [PATCH 03/16] test(rover-handler): update tests to ensure authentication remains unchanged for invalid clientAuthMethod values --- rover-ctl/pkg/handlers/v0/rover.go | 6 ------ rover-ctl/pkg/handlers/v0/rover_test.go | 28 +++++++++++++++++++++---- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/rover-ctl/pkg/handlers/v0/rover.go b/rover-ctl/pkg/handlers/v0/rover.go index bdf14d006..e915bfeb1 100644 --- a/rover-ctl/pkg/handlers/v0/rover.go +++ b/rover-ctl/pkg/handlers/v0/rover.go @@ -90,35 +90,29 @@ func PatchAuthentication(spec map[string]any) { } authMap, ok := auth.(map[string]any) if !ok { - delete(spec, "authentication") return } m2m, exists := authMap["m2m"] if !exists { - delete(spec, "authentication") return } m2mMap, ok := m2m.(map[string]any) if !ok { - delete(spec, "authentication") return } clientAuthMethod, exists := m2mMap["clientAuthMethod"] if !exists { - delete(spec, "authentication") return } clientAuthMethodStr, ok := clientAuthMethod.(string) if !ok { - delete(spec, "authentication") return } apiValue, valid := clientAuthMethodMapping[strings.ToLower(clientAuthMethodStr)] if !valid { - delete(spec, "authentication") return } diff --git a/rover-ctl/pkg/handlers/v0/rover_test.go b/rover-ctl/pkg/handlers/v0/rover_test.go index eb654d5f4..347b25fb4 100644 --- a/rover-ctl/pkg/handlers/v0/rover_test.go +++ b/rover-ctl/pkg/handlers/v0/rover_test.go @@ -440,7 +440,7 @@ var _ = Describe("Rover Handler", func() { Expect(content).NotTo(HaveKey("authentication")) }) - It("should drop authentication when clientAuthMethod has invalid value", func() { + It("should leave authentication untouched when clientAuthMethod has invalid value", func() { obj := &types.UnstructuredObject{ Content: map[string]any{ "spec": map[string]any{ @@ -457,10 +457,10 @@ var _ = Describe("Rover Handler", func() { Expect(err).NotTo(HaveOccurred()) content := obj.GetContent() - Expect(content).NotTo(HaveKey("authentication")) + Expect(content).To(HaveKey("authentication")) }) - It("should drop authentication when authentication format is invalid", func() { + It("should leave authentication untouched when format is not a map", func() { obj := &types.UnstructuredObject{ Content: map[string]any{ "spec": map[string]any{ @@ -473,7 +473,27 @@ var _ = Describe("Rover Handler", func() { Expect(err).NotTo(HaveOccurred()) content := obj.GetContent() - Expect(content).NotTo(HaveKey("authentication")) + Expect(content).To(HaveKey("authentication")) + }) + + It("should leave authentication untouched when already in rover-server format", func() { + obj := &types.UnstructuredObject{ + Content: map[string]any{ + "spec": map[string]any{ + "authentication": map[string]any{ + "clientAuthMethod": "BASIC", + }, + }, + }, + } + + err := v0.PatchRoverRequest(context.Background(), obj) + Expect(err).NotTo(HaveOccurred()) + + content := obj.GetContent() + auth, ok := content["authentication"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(auth["clientAuthMethod"]).To(Equal("BASIC")) }) }) From dad4b83c84a1212de732d26eac7f2dea9cbe2058 Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Mon, 27 Apr 2026 11:45:16 +0200 Subject: [PATCH 04/16] fix(timestamps): update snapshot timestamps to reflect correct timezone offsets --- .../controller/__snapshots__/suite_controller_test.snap | 4 ++-- .../internal/mapper/status/__snapshots__/status_test.snap | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rover-server/internal/controller/__snapshots__/suite_controller_test.snap b/rover-server/internal/controller/__snapshots__/suite_controller_test.snap index 8b5d78daa..59f6fc1f3 100755 --- a/rover-server/internal/controller/__snapshots__/suite_controller_test.snap +++ b/rover-server/internal/controller/__snapshots__/suite_controller_test.snap @@ -617,7 +617,7 @@ [Rover Controller Get rover status should return the status of a rover successfully - 1] { - "createdAt": "2025-09-18T08:39:44Z", + "createdAt": "2025-09-18T10:39:44+02:00", "errors": [ { "cause": "NoApproval", @@ -632,7 +632,7 @@ } ], "overallStatus": "blocked", - "processedAt": "2025-10-08T07:16:40Z", + "processedAt": "2025-10-08T09:16:40+02:00", "processingState": "done", "state": "blocked" } diff --git a/rover-server/internal/mapper/status/__snapshots__/status_test.snap b/rover-server/internal/mapper/status/__snapshots__/status_test.snap index 5e2982038..3f445fede 100755 --- a/rover-server/internal/mapper/status/__snapshots__/status_test.snap +++ b/rover-server/internal/mapper/status/__snapshots__/status_test.snap @@ -19,7 +19,7 @@ [Rover Status Mapper MapRoverResponse must map rover response correctly - 1] { - "createdAt": "2025-09-18T08:39:44Z", + "createdAt": "2025-09-18T10:39:44+02:00", "errors": [ { "cause": "NoApproval", @@ -34,7 +34,7 @@ } ], "overallStatus": "blocked", - "processedAt": "2025-10-08T07:16:40Z", + "processedAt": "2025-10-08T09:16:40+02:00", "processingState": "done", "state": "blocked" } From 9bc451152a032af07f5cac39333870d350ae1635 Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Mon, 27 Apr 2026 15:09:59 +0200 Subject: [PATCH 05/16] feat(rover): implement mapping for Rover authentication methods and add corresponding tests Co-authored-by: Copilot --- .../rover/in/__snapshots__/rover_test.snap | 9 ++++ .../internal/mapper/rover/in/rover.go | 13 +++++ .../internal/mapper/rover/in/rover_test.go | 48 +++++++++++++++++++ .../internal/mapper/rover/out/rover.go | 14 ++++++ .../internal/mapper/rover/out/rover_test.go | 44 +++++++++++++++++ rover/api/v1/rover_types.go | 22 +++++++++ 6 files changed, 150 insertions(+) diff --git a/rover-server/internal/mapper/rover/in/__snapshots__/rover_test.snap b/rover-server/internal/mapper/rover/in/__snapshots__/rover_test.snap index 998f6e3f4..60a42884a 100755 --- a/rover-server/internal/mapper/rover/in/__snapshots__/rover_test.snap +++ b/rover-server/internal/mapper/rover/in/__snapshots__/rover_test.snap @@ -6,6 +6,7 @@ Spec: v1.RoverSpec{ Zone: "zone", IpRestrictions: (*v1.IpRestrictions)(nil), + Authentication: (*v1.RoverAuthentication)(nil), ClientSecret: "", Exposures: { { @@ -51,6 +52,7 @@ Spec: v1.RoverSpec{ Zone: "", IpRestrictions: (*v1.IpRestrictions)(nil), + Authentication: (*v1.RoverAuthentication)(nil), ClientSecret: "", Exposures: { { @@ -85,6 +87,7 @@ Spec: v1.RoverSpec{ Zone: "", IpRestrictions: (*v1.IpRestrictions)(nil), + Authentication: (*v1.RoverAuthentication)(nil), ClientSecret: "", Exposures: nil, Subscriptions: { @@ -128,6 +131,7 @@ Spec: v1.RoverSpec{ Zone: "zone", IpRestrictions: (*v1.IpRestrictions)(nil), + Authentication: (*v1.RoverAuthentication)(nil), ClientSecret: "", Exposures: { { @@ -214,6 +218,7 @@ Spec: v1.RoverSpec{ Zone: "zone", IpRestrictions: (*v1.IpRestrictions)(nil), + Authentication: (*v1.RoverAuthentication)(nil), ClientSecret: "", Exposures: { { @@ -275,6 +280,7 @@ Spec: v1.RoverSpec{ Zone: "zone", IpRestrictions: (*v1.IpRestrictions)(nil), + Authentication: (*v1.RoverAuthentication)(nil), ClientSecret: "supersecret", Exposures: { { @@ -320,6 +326,7 @@ Spec: v1.RoverSpec{ Zone: "", IpRestrictions: (*v1.IpRestrictions)(nil), + Authentication: (*v1.RoverAuthentication)(nil), ClientSecret: "", Exposures: nil, Subscriptions: nil, @@ -343,6 +350,7 @@ Spec: v1.RoverSpec{ Zone: "", IpRestrictions: (*v1.IpRestrictions)(nil), + Authentication: (*v1.RoverAuthentication)(nil), ClientSecret: "", Exposures: nil, Subscriptions: nil, @@ -377,6 +385,7 @@ Spec: v1.RoverSpec{ Zone: "", IpRestrictions: (*v1.IpRestrictions)(nil), + Authentication: (*v1.RoverAuthentication)(nil), ClientSecret: "", Exposures: nil, Subscriptions: nil, diff --git a/rover-server/internal/mapper/rover/in/rover.go b/rover-server/internal/mapper/rover/in/rover.go index 38285e8af..971234da5 100644 --- a/rover-server/internal/mapper/rover/in/rover.go +++ b/rover-server/internal/mapper/rover/in/rover.go @@ -72,6 +72,7 @@ func MapRover(in *api.Rover, out *roverv1.Rover) error { Allow: in.IpRestrictions.Allow, } } + mapAuthentication(in, out) return nil } @@ -143,3 +144,15 @@ func mapPermissions(in *api.Rover, out *roverv1.Rover) error { return nil } + +func mapAuthentication(in *api.Rover, out *roverv1.Rover) { + method := string(in.Authentication.ClientAuthMethod) + if method == "" { + return + } + out.Spec.Authentication = &roverv1.RoverAuthentication{ + M2M: &roverv1.RoverM2MAuthentication{ + ClientAuthMethod: method, + }, + } +} diff --git a/rover-server/internal/mapper/rover/in/rover_test.go b/rover-server/internal/mapper/rover/in/rover_test.go index a09bf4fcb..869bcfb47 100644 --- a/rover-server/internal/mapper/rover/in/rover_test.go +++ b/rover-server/internal/mapper/rover/in/rover_test.go @@ -140,6 +140,54 @@ var _ = Describe("Rover Mapper", func() { }) }) + Context("MapAuthentication", func() { + It("must map BASIC to CRD", func() { + input := &api.Rover{ + Zone: "zone", + Authentication: api.Authentication{ + ClientAuthMethod: api.BASIC, + }, + } + output := &roverv1.Rover{} + + err := MapRover(input, output) + + Expect(err).ToNot(HaveOccurred()) + Expect(output.Spec.Authentication).ToNot(BeNil()) + Expect(output.Spec.Authentication.M2M).ToNot(BeNil()) + Expect(output.Spec.Authentication.M2M.ClientAuthMethod).To(Equal("BASIC")) + }) + + It("must map POST to CRD", func() { + input := &api.Rover{ + Zone: "zone", + Authentication: api.Authentication{ + ClientAuthMethod: api.POST, + }, + } + output := &roverv1.Rover{} + + err := MapRover(input, output) + + Expect(err).ToNot(HaveOccurred()) + Expect(output.Spec.Authentication).ToNot(BeNil()) + Expect(output.Spec.Authentication.M2M).ToNot(BeNil()) + Expect(output.Spec.Authentication.M2M.ClientAuthMethod).To(Equal("POST")) + }) + + It("must not set authentication when clientAuthMethod is empty", func() { + input := &api.Rover{ + Zone: "zone", + } + output := &roverv1.Rover{} + + err := MapRover(input, output) + + Expect(err).ToNot(HaveOccurred()) + Expect(output.Spec.Authentication).To(BeNil()) + }) + }) + Context("MapRequest", func() { It("must map a RoverUpdateRequest to a Rover correctly", func() { output, err := MapRequest(roverUpdateRequest, resourceIdInfo) diff --git a/rover-server/internal/mapper/rover/out/rover.go b/rover-server/internal/mapper/rover/out/rover.go index 1663bc036..2364dd450 100644 --- a/rover-server/internal/mapper/rover/out/rover.go +++ b/rover-server/internal/mapper/rover/out/rover.go @@ -43,9 +43,23 @@ func MapRover(in *roverv1.Rover, out *api.Rover) error { } out.Zone = in.Spec.Zone + mapAuthentication(in, out) return nil } +func mapAuthentication(in *roverv1.Rover, out *api.Rover) { + if in.Spec.Authentication == nil || in.Spec.Authentication.M2M == nil { + return + } + method := in.Spec.Authentication.M2M.ClientAuthMethod + if method == "" { + return + } + out.Authentication = api.Authentication{ + ClientAuthMethod: api.AuthenticationClientAuthMethod(method), + } +} + func mapExposures(in *roverv1.Rover, out *api.Rover) error { if in == nil { return errors.New("input rover is nil") diff --git a/rover-server/internal/mapper/rover/out/rover_test.go b/rover-server/internal/mapper/rover/out/rover_test.go index 34326ba4e..cd1ee490f 100644 --- a/rover-server/internal/mapper/rover/out/rover_test.go +++ b/rover-server/internal/mapper/rover/out/rover_test.go @@ -10,6 +10,7 @@ import ( . "github.com/onsi/gomega" "github.com/telekom/controlplane/rover-server/internal/api" + roverv1 "github.com/telekom/controlplane/rover/api/v1" ) var _ = Describe("Rover Mapper", func() { @@ -59,6 +60,49 @@ var _ = Describe("Rover Mapper", func() { }) }) + Context("MapAuthentication", func() { + It("must map BASIC from CRD to API", func() { + input := rover.DeepCopy() + input.Spec.Authentication = &roverv1.RoverAuthentication{ + M2M: &roverv1.RoverM2MAuthentication{ + ClientAuthMethod: "BASIC", + }, + } + output := &api.Rover{} + + err := MapRover(input, output) + + Expect(err).ToNot(HaveOccurred()) + Expect(output.Authentication.ClientAuthMethod).To(Equal(api.BASIC)) + }) + + It("must map POST from CRD to API", func() { + input := rover.DeepCopy() + input.Spec.Authentication = &roverv1.RoverAuthentication{ + M2M: &roverv1.RoverM2MAuthentication{ + ClientAuthMethod: "POST", + }, + } + output := &api.Rover{} + + err := MapRover(input, output) + + Expect(err).ToNot(HaveOccurred()) + Expect(output.Authentication.ClientAuthMethod).To(Equal(api.POST)) + }) + + It("must not set authentication when it is nil", func() { + input := rover.DeepCopy() + input.Spec.Authentication = nil + output := &api.Rover{} + + err := MapRover(input, output) + + Expect(err).ToNot(HaveOccurred()) + Expect(output.Authentication).To(Equal(api.Authentication{})) + }) + }) + Context("MapRoverResponse", func() { It("must map a Rover to a RoverResponse correctly", func() { input := GetRoverWithReadyCondition(rover) diff --git a/rover/api/v1/rover_types.go b/rover/api/v1/rover_types.go index 8e9004b89..7dd56923a 100644 --- a/rover/api/v1/rover_types.go +++ b/rover/api/v1/rover_types.go @@ -100,6 +100,10 @@ type RoverSpec struct { // +kubebuilder:validation:Optional IpRestrictions *IpRestrictions `json:"ipRestrictions,omitempty"` + // Authentication defines the authentication configuration for this application + // +kubebuilder:validation:Optional + Authentication *RoverAuthentication `json:"authentication,omitempty"` + // ClientSecret is the secret used for client authentication // If not specified, a randomly generated secret will be used // +kubebuilder:validation:Optional @@ -176,6 +180,24 @@ type IpRestrictions struct { Deny []string `json:"deny,omitempty"` } +// RoverAuthentication defines the top-level authentication configuration for a Rover application +type RoverAuthentication struct { + // M2M defines machine-to-machine authentication settings for the application + // +kubebuilder:validation:Optional + M2M *RoverM2MAuthentication `json:"m2m,omitempty"` +} + +// RoverM2MAuthentication defines the M2M authentication settings +type RoverM2MAuthentication struct { + // ClientAuthMethod configures the client authentication method, according to RFC 6749 + // This feature is currently only documented but not parsed towards the application and identity domain as it is still in discussion whether + // this should will be enforced for IDPs. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum=NONE;POST;BASIC + // +kubebuilder:default=BASIC + ClientAuthMethod string `json:"clientAuthMethod,omitempty"` +} + // Exposure defines a service that is exposed by this Rover // +kubebuilder:validation:XValidation:rule="self == null || has(self.api) || has(self.event)", message="At least one of api or event must be specified" // +kubebuilder:validation:XValidation:rule="self == null || (!has(self.api) && has(self.event)) || (has(self.api) && !has(self.event))", message="Only one of api or event can be specified (XOR relationship)" From d9fafe0a07e404f02b7767c780e6f893c42f2a66 Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Mon, 27 Apr 2026 15:33:31 +0200 Subject: [PATCH 06/16] chore: make generate & manifest --- rover/api/v1/zz_generated.deepcopy.go | 40 +++++++++++++++++++ .../bases/rover.cp.ei.telekom.de_rovers.yaml | 21 ++++++++++ 2 files changed, 61 insertions(+) diff --git a/rover/api/v1/zz_generated.deepcopy.go b/rover/api/v1/zz_generated.deepcopy.go index c9e35d45c..0f79ebcb6 100644 --- a/rover/api/v1/zz_generated.deepcopy.go +++ b/rover/api/v1/zz_generated.deepcopy.go @@ -896,6 +896,26 @@ func (in *Rover) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoverAuthentication) DeepCopyInto(out *RoverAuthentication) { + *out = *in + if in.M2M != nil { + in, out := &in.M2M, &out.M2M + *out = new(RoverM2MAuthentication) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoverAuthentication. +func (in *RoverAuthentication) DeepCopy() *RoverAuthentication { + if in == nil { + return nil + } + out := new(RoverAuthentication) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RoverList) DeepCopyInto(out *RoverList) { *out = *in @@ -928,6 +948,21 @@ func (in *RoverList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoverM2MAuthentication) DeepCopyInto(out *RoverM2MAuthentication) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoverM2MAuthentication. +func (in *RoverM2MAuthentication) DeepCopy() *RoverM2MAuthentication { + if in == nil { + return nil + } + out := new(RoverM2MAuthentication) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RoverSpec) DeepCopyInto(out *RoverSpec) { *out = *in @@ -936,6 +971,11 @@ func (in *RoverSpec) DeepCopyInto(out *RoverSpec) { *out = new(IpRestrictions) (*in).DeepCopyInto(*out) } + if in.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *out = new(RoverAuthentication) + (*in).DeepCopyInto(*out) + } if in.Exposures != nil { in, out := &in.Exposures, &out.Exposures *out = make([]Exposure, len(*in)) diff --git a/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml b/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml index 0bf9dc969..0367b10f6 100644 --- a/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml +++ b/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml @@ -49,6 +49,27 @@ spec: spec: description: Spec defines the desired state of the Rover resource properties: + authentication: + description: Authentication defines the authentication configuration + for this application + properties: + m2m: + description: M2M defines machine-to-machine authentication settings + for the application + properties: + clientAuthMethod: + default: BASIC + description: |- + ClientAuthMethod configures the client authentication method, according to RFC 6749 + This feature is currently only documented but not parsed towards the application and identity domain as it is still in discussion whether + this should will be enforced for IDPs. + enum: + - NONE + - POST + - BASIC + type: string + type: object + type: object clientSecret: description: |- ClientSecret is the secret used for client authentication From 63a4d66e30e5205ef440962eaa29e84c7dd80d72 Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Tue, 28 Apr 2026 08:20:51 +0200 Subject: [PATCH 07/16] refactor: align with tokenRequest values --- rover-server/internal/mapper/rover/in/rover.go | 14 ++++++++++++-- .../internal/mapper/rover/in/rover_test.go | 8 ++++---- rover-server/internal/mapper/rover/out/rover.go | 16 +++++++++++++--- .../internal/mapper/rover/out/rover_test.go | 8 ++++---- rover/api/v1/rover_types.go | 8 ++++---- 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/rover-server/internal/mapper/rover/in/rover.go b/rover-server/internal/mapper/rover/in/rover.go index 971234da5..be979c1b3 100644 --- a/rover-server/internal/mapper/rover/in/rover.go +++ b/rover-server/internal/mapper/rover/in/rover.go @@ -145,14 +145,24 @@ func mapPermissions(in *api.Rover, out *roverv1.Rover) error { return nil } +// clientAuthMethodToCRD maps rover-server API enum values to rover CRD tokenRequest values. +var clientAuthMethodToCRD = map[api.AuthenticationClientAuthMethod]string{ + api.BASIC: "header", + api.POST: "body", +} + func mapAuthentication(in *api.Rover, out *roverv1.Rover) { - method := string(in.Authentication.ClientAuthMethod) + method := in.Authentication.ClientAuthMethod if method == "" { return } + tokenRequest, ok := clientAuthMethodToCRD[method] + if !ok { + return + } out.Spec.Authentication = &roverv1.RoverAuthentication{ M2M: &roverv1.RoverM2MAuthentication{ - ClientAuthMethod: method, + TokenRequest: tokenRequest, }, } } diff --git a/rover-server/internal/mapper/rover/in/rover_test.go b/rover-server/internal/mapper/rover/in/rover_test.go index 869bcfb47..3f127adb0 100644 --- a/rover-server/internal/mapper/rover/in/rover_test.go +++ b/rover-server/internal/mapper/rover/in/rover_test.go @@ -141,7 +141,7 @@ var _ = Describe("Rover Mapper", func() { }) Context("MapAuthentication", func() { - It("must map BASIC to CRD", func() { + It("must map BASIC to header in CRD", func() { input := &api.Rover{ Zone: "zone", Authentication: api.Authentication{ @@ -155,10 +155,10 @@ var _ = Describe("Rover Mapper", func() { Expect(err).ToNot(HaveOccurred()) Expect(output.Spec.Authentication).ToNot(BeNil()) Expect(output.Spec.Authentication.M2M).ToNot(BeNil()) - Expect(output.Spec.Authentication.M2M.ClientAuthMethod).To(Equal("BASIC")) + Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("header")) }) - It("must map POST to CRD", func() { + It("must map POST to body in CRD", func() { input := &api.Rover{ Zone: "zone", Authentication: api.Authentication{ @@ -172,7 +172,7 @@ var _ = Describe("Rover Mapper", func() { Expect(err).ToNot(HaveOccurred()) Expect(output.Spec.Authentication).ToNot(BeNil()) Expect(output.Spec.Authentication.M2M).ToNot(BeNil()) - Expect(output.Spec.Authentication.M2M.ClientAuthMethod).To(Equal("POST")) + Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("body")) }) It("must not set authentication when clientAuthMethod is empty", func() { diff --git a/rover-server/internal/mapper/rover/out/rover.go b/rover-server/internal/mapper/rover/out/rover.go index 2364dd450..bba814a43 100644 --- a/rover-server/internal/mapper/rover/out/rover.go +++ b/rover-server/internal/mapper/rover/out/rover.go @@ -47,16 +47,26 @@ func MapRover(in *roverv1.Rover, out *api.Rover) error { return nil } +// tokenRequestToAPI maps rover CRD tokenRequest values to rover-server API enum values. +var tokenRequestToAPI = map[string]api.AuthenticationClientAuthMethod{ + "header": api.BASIC, + "body": api.POST, +} + func mapAuthentication(in *roverv1.Rover, out *api.Rover) { if in.Spec.Authentication == nil || in.Spec.Authentication.M2M == nil { return } - method := in.Spec.Authentication.M2M.ClientAuthMethod - if method == "" { + tokenRequest := in.Spec.Authentication.M2M.TokenRequest + if tokenRequest == "" { + return + } + method, ok := tokenRequestToAPI[tokenRequest] + if !ok { return } out.Authentication = api.Authentication{ - ClientAuthMethod: api.AuthenticationClientAuthMethod(method), + ClientAuthMethod: method, } } diff --git a/rover-server/internal/mapper/rover/out/rover_test.go b/rover-server/internal/mapper/rover/out/rover_test.go index cd1ee490f..09060a5e4 100644 --- a/rover-server/internal/mapper/rover/out/rover_test.go +++ b/rover-server/internal/mapper/rover/out/rover_test.go @@ -61,11 +61,11 @@ var _ = Describe("Rover Mapper", func() { }) Context("MapAuthentication", func() { - It("must map BASIC from CRD to API", func() { + It("must map header from CRD to BASIC in API", func() { input := rover.DeepCopy() input.Spec.Authentication = &roverv1.RoverAuthentication{ M2M: &roverv1.RoverM2MAuthentication{ - ClientAuthMethod: "BASIC", + TokenRequest: "header", }, } output := &api.Rover{} @@ -76,11 +76,11 @@ var _ = Describe("Rover Mapper", func() { Expect(output.Authentication.ClientAuthMethod).To(Equal(api.BASIC)) }) - It("must map POST from CRD to API", func() { + It("must map body from CRD to POST in API", func() { input := rover.DeepCopy() input.Spec.Authentication = &roverv1.RoverAuthentication{ M2M: &roverv1.RoverM2MAuthentication{ - ClientAuthMethod: "POST", + TokenRequest: "body", }, } output := &api.Rover{} diff --git a/rover/api/v1/rover_types.go b/rover/api/v1/rover_types.go index 7dd56923a..568f7fd58 100644 --- a/rover/api/v1/rover_types.go +++ b/rover/api/v1/rover_types.go @@ -189,13 +189,13 @@ type RoverAuthentication struct { // RoverM2MAuthentication defines the M2M authentication settings type RoverM2MAuthentication struct { - // ClientAuthMethod configures the client authentication method, according to RFC 6749 + // TokenRequest configures the client authentication method, according to RFC 6749 // This feature is currently only documented but not parsed towards the application and identity domain as it is still in discussion whether // this should will be enforced for IDPs. // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=NONE;POST;BASIC - // +kubebuilder:default=BASIC - ClientAuthMethod string `json:"clientAuthMethod,omitempty"` + // +kubebuilder:validation:Enum=body;header + // +kubebuilder:default=header + TokenRequest string `json:"tokenRequest,omitempty"` } // Exposure defines a service that is exposed by this Rover From 59b6a0a1be91722dcf0d6ea5ed9a2f6edf0b44c0 Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Tue, 28 Apr 2026 10:19:04 +0200 Subject: [PATCH 08/16] chore: make generate & manifests --- .../crd/bases/rover.cp.ei.telekom.de_rovers.yaml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml b/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml index 0367b10f6..685af5c63 100644 --- a/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml +++ b/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml @@ -57,16 +57,15 @@ spec: description: M2M defines machine-to-machine authentication settings for the application properties: - clientAuthMethod: - default: BASIC + tokenRequest: + default: header description: |- - ClientAuthMethod configures the client authentication method, according to RFC 6749 + TokenRequest configures the client authentication method, according to RFC 6749 This feature is currently only documented but not parsed towards the application and identity domain as it is still in discussion whether this should will be enforced for IDPs. enum: - - NONE - - POST - - BASIC + - body + - header type: string type: object type: object From 8de6c6604849974d18102a2a5e7b40376250c247 Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Tue, 28 Apr 2026 10:19:41 +0200 Subject: [PATCH 09/16] refactor: move to fuzzbuzz converter --- rover-ctl/pkg/handlers/v0/rover.go | 25 +++----------- rover-ctl/pkg/handlers/v0/rover_test.go | 12 ++++--- .../internal/mapper/rover/in/fuzzy_match.go | 19 ++++++++++- .../mapper/rover/in/fuzzy_match_test.go | 25 ++++++++++++++ .../internal/mapper/rover/in/rover.go | 2 +- .../internal/mapper/rover/in/rover_test.go | 34 +++++++++++++++++++ 6 files changed, 90 insertions(+), 27 deletions(-) diff --git a/rover-ctl/pkg/handlers/v0/rover.go b/rover-ctl/pkg/handlers/v0/rover.go index e915bfeb1..8172660a5 100644 --- a/rover-ctl/pkg/handlers/v0/rover.go +++ b/rover-ctl/pkg/handlers/v0/rover.go @@ -9,7 +9,6 @@ import ( "encoding/json" "maps" "net/http" - "strings" "github.com/pkg/errors" "github.com/telekom/controlplane/rover-ctl/pkg/handlers/common" @@ -74,15 +73,10 @@ func PatchRoverRequest(ctx context.Context, obj types.Object) error { return nil } -// clientAuthMethodMapping maps rover-ctl YAML values to rover-server API enum values. -var clientAuthMethodMapping = map[string]string{ - "basic": "BASIC", - "body": "POST", // Configures client authentication method, according to RFC 6749 -} - -// PatchAuthentication maps spec.authentication.m2m.clientAuthMethod from the rover-ctl -// YAML format (basic/body) to the rover-server API format (BASIC/POST) and restructures -// it into spec.authentication.clientAuthMethod. +// PatchAuthentication restructures spec.authentication.m2m.clientAuthMethod +// into spec.authentication.clientAuthMethod for the rover-server API format. +// Value normalization (e.g. basic→BASIC, body→POST) is handled server-side +// by FuzzyMatchClientAuthMethod. func PatchAuthentication(spec map[string]any) { auth, exists := spec["authentication"] if !exists { @@ -106,18 +100,9 @@ func PatchAuthentication(spec map[string]any) { if !exists { return } - clientAuthMethodStr, ok := clientAuthMethod.(string) - if !ok { - return - } - - apiValue, valid := clientAuthMethodMapping[strings.ToLower(clientAuthMethodStr)] - if !valid { - return - } spec["authentication"] = map[string]any{ - "clientAuthMethod": apiValue, + "clientAuthMethod": clientAuthMethod, } } diff --git a/rover-ctl/pkg/handlers/v0/rover_test.go b/rover-ctl/pkg/handlers/v0/rover_test.go index 347b25fb4..73f276128 100644 --- a/rover-ctl/pkg/handlers/v0/rover_test.go +++ b/rover-ctl/pkg/handlers/v0/rover_test.go @@ -382,7 +382,7 @@ var _ = Describe("Rover Handler", func() { }) Describe("PatchAuthentication", func() { - It("should map 'basic' to 'BASIC' in authentication.clientAuthMethod", func() { + It("should pass through 'basic' as-is in authentication.clientAuthMethod", func() { obj := &types.UnstructuredObject{ Content: map[string]any{ "spec": map[string]any{ @@ -401,10 +401,10 @@ var _ = Describe("Rover Handler", func() { content := obj.GetContent() auth, ok := content["authentication"].(map[string]any) Expect(ok).To(BeTrue()) - Expect(auth["clientAuthMethod"]).To(Equal("BASIC")) + Expect(auth["clientAuthMethod"]).To(Equal("basic")) }) - It("should map 'body' to 'POST' in authentication.clientAuthMethod", func() { + It("should pass through 'body' as-is in authentication.clientAuthMethod", func() { obj := &types.UnstructuredObject{ Content: map[string]any{ "spec": map[string]any{ @@ -423,7 +423,7 @@ var _ = Describe("Rover Handler", func() { content := obj.GetContent() auth, ok := content["authentication"].(map[string]any) Expect(ok).To(BeTrue()) - Expect(auth["clientAuthMethod"]).To(Equal("POST")) + Expect(auth["clientAuthMethod"]).To(Equal("body")) }) It("should not add authentication when it is missing", func() { @@ -457,7 +457,9 @@ var _ = Describe("Rover Handler", func() { Expect(err).NotTo(HaveOccurred()) content := obj.GetContent() - Expect(content).To(HaveKey("authentication")) + auth, ok := content["authentication"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(auth["clientAuthMethod"]).To(Equal("invalid")) }) It("should leave authentication untouched when format is not a map", func() { diff --git a/rover-server/internal/mapper/rover/in/fuzzy_match.go b/rover-server/internal/mapper/rover/in/fuzzy_match.go index 9b1c948c8..a15dee93e 100644 --- a/rover-server/internal/mapper/rover/in/fuzzy_match.go +++ b/rover-server/internal/mapper/rover/in/fuzzy_match.go @@ -4,7 +4,10 @@ package in -import roverv1 "github.com/telekom/controlplane/rover/api/v1" +import ( + "github.com/telekom/controlplane/rover-server/internal/api" + roverv1 "github.com/telekom/controlplane/rover/api/v1" +) // FuzzyMatchEventDeliveryType performs a fuzzy match on the input string to determine the EventDeliveryType. func FuzzyMatchEventDeliveryType(in string) roverv1.EventDeliveryType { @@ -41,3 +44,17 @@ func FuzzyMatchEventResponseFilterMode(in string) roverv1.EventResponseFilterMod return roverv1.EventResponseFilterMode(in) } } + +// FuzzyMatchClientAuthMethod performs a fuzzy match on the input string to determine the AuthenticationClientAuthMethod. +func FuzzyMatchClientAuthMethod(in string) api.AuthenticationClientAuthMethod { + switch in { + case "basic", "Basic", "BASIC": + return api.BASIC + case "body", "Body", "BODY", "post", "Post", "POST": + return api.POST + case "none", "None", "NONE": + return api.NONE + default: + return api.AuthenticationClientAuthMethod(in) + } +} diff --git a/rover-server/internal/mapper/rover/in/fuzzy_match_test.go b/rover-server/internal/mapper/rover/in/fuzzy_match_test.go index 5b03e6fdd..863a4c657 100644 --- a/rover-server/internal/mapper/rover/in/fuzzy_match_test.go +++ b/rover-server/internal/mapper/rover/in/fuzzy_match_test.go @@ -7,6 +7,7 @@ package in import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/telekom/controlplane/rover-server/internal/api" roverv1 "github.com/telekom/controlplane/rover/api/v1" ) @@ -63,3 +64,27 @@ var _ = DescribeTable("FuzzyMatchEventResponseFilterMode", Entry("unknown passthrough", "filter", roverv1.EventResponseFilterMode("filter")), Entry("empty passthrough", "", roverv1.EventResponseFilterMode("")), ) + +var _ = DescribeTable("FuzzyMatchClientAuthMethod", + func(input string, expected api.AuthenticationClientAuthMethod) { + Expect(FuzzyMatchClientAuthMethod(input)).To(Equal(expected)) + }, + // BASIC variants + Entry("basic", "basic", api.BASIC), + Entry("Basic", "Basic", api.BASIC), + Entry("BASIC", "BASIC", api.BASIC), + // POST variants + Entry("body", "body", api.POST), + Entry("Body", "Body", api.POST), + Entry("BODY", "BODY", api.POST), + Entry("post", "post", api.POST), + Entry("Post", "Post", api.POST), + Entry("POST", "POST", api.POST), + // NONE variants + Entry("none", "none", api.NONE), + Entry("None", "None", api.NONE), + Entry("NONE", "NONE", api.NONE), + // Default passthrough + Entry("unknown passthrough", "unknown", api.AuthenticationClientAuthMethod("unknown")), + Entry("empty passthrough", "", api.AuthenticationClientAuthMethod("")), +) diff --git a/rover-server/internal/mapper/rover/in/rover.go b/rover-server/internal/mapper/rover/in/rover.go index be979c1b3..940b240bd 100644 --- a/rover-server/internal/mapper/rover/in/rover.go +++ b/rover-server/internal/mapper/rover/in/rover.go @@ -152,7 +152,7 @@ var clientAuthMethodToCRD = map[api.AuthenticationClientAuthMethod]string{ } func mapAuthentication(in *api.Rover, out *roverv1.Rover) { - method := in.Authentication.ClientAuthMethod + method := FuzzyMatchClientAuthMethod(string(in.Authentication.ClientAuthMethod)) if method == "" { return } diff --git a/rover-server/internal/mapper/rover/in/rover_test.go b/rover-server/internal/mapper/rover/in/rover_test.go index 3f127adb0..5bd5222b5 100644 --- a/rover-server/internal/mapper/rover/in/rover_test.go +++ b/rover-server/internal/mapper/rover/in/rover_test.go @@ -186,6 +186,40 @@ var _ = Describe("Rover Mapper", func() { Expect(err).ToNot(HaveOccurred()) Expect(output.Spec.Authentication).To(BeNil()) }) + + It("must fuzzy-match 'basic' to header in CRD", func() { + input := &api.Rover{ + Zone: "zone", + Authentication: api.Authentication{ + ClientAuthMethod: "basic", + }, + } + output := &roverv1.Rover{} + + err := MapRover(input, output) + + Expect(err).ToNot(HaveOccurred()) + Expect(output.Spec.Authentication).ToNot(BeNil()) + Expect(output.Spec.Authentication.M2M).ToNot(BeNil()) + Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("header")) + }) + + It("must fuzzy-match 'body' to body in CRD", func() { + input := &api.Rover{ + Zone: "zone", + Authentication: api.Authentication{ + ClientAuthMethod: "body", + }, + } + output := &roverv1.Rover{} + + err := MapRover(input, output) + + Expect(err).ToNot(HaveOccurred()) + Expect(output.Spec.Authentication).ToNot(BeNil()) + Expect(output.Spec.Authentication.M2M).ToNot(BeNil()) + Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("body")) + }) }) Context("MapRequest", func() { From 10ae067c733a2c8735185818d0b6ace6971f2986 Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Tue, 28 Apr 2026 13:53:44 +0200 Subject: [PATCH 10/16] refactor: align tokenRequests values for RFC 7591 Co-authored-by: Copilot --- api/api/v1/security_types.go | 4 ++-- .../api.cp.ei.telekom.de_apiexposures.yaml | 8 ++++---- .../controller/apiexposure_controller_test.go | 8 ++++---- ...bscription_controller_ratelimiting_test.go | 2 +- .../internal/mapper/apiexposure/out.go | 14 +++++++++++++- .../internal/mapper/apiexposure/out_test.go | 4 ++-- gateway/api/v1/security_types.go | 4 ++-- .../gateway.cp.ei.telekom.de_routes.yaml | 16 ++++++++-------- .../controller/route_controller_test.go | 2 +- .../features/builder_external_idp_test.go | 6 +++--- .../internal/features/feature/external_idp.go | 19 ++++++++++++++++++- .../__snapshots__/exposure_security_test.snap | 4 ++-- .../internal/mapper/rover/in/exposure.go | 16 +++++++++++++++- .../mapper/rover/in/exposure_security_test.go | 2 +- .../internal/mapper/rover/in/rover.go | 4 ++-- .../internal/mapper/rover/in/rover_test.go | 16 ++++++++-------- .../__snapshots__/exposure_security_test.snap | 2 +- .../internal/mapper/rover/out/exposure.go | 15 ++++++++++++++- .../rover/out/exposure_security_test.go | 4 ++-- .../internal/mapper/rover/out/rover.go | 4 ++-- .../internal/mapper/rover/out/rover_test.go | 8 ++++---- rover/api/v1/rover_types.go | 6 +++--- rover/api/v1/security_types.go | 4 ++-- .../bases/rover.cp.ei.telekom.de_rovers.yaml | 16 ++++++++-------- .../controller/rover_controller_test.go | 4 ++-- 25 files changed, 124 insertions(+), 68 deletions(-) diff --git a/api/api/v1/security_types.go b/api/api/v1/security_types.go index 4c8f4fc13..cc73ab590 100644 --- a/api/api/v1/security_types.go +++ b/api/api/v1/security_types.go @@ -64,9 +64,9 @@ type ExternalIdentityProvider struct { // +kubebuilder:validation:Format=uri TokenEndpoint string `json:"tokenEndpoint"` - // TokenRequest is the type of token request, "body" or "header" + // TokenRequest configures the token endpoint authentication method (RFC 7591) // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=body;header + // +kubebuilder:validation:Enum=client_secret_basic;client_secret_post TokenRequest string `json:"tokenRequest,omitempty"` // GrantType defines the OAuth2 grant type to use for the token request diff --git a/api/config/crd/bases/api.cp.ei.telekom.de_apiexposures.yaml b/api/config/crd/bases/api.cp.ei.telekom.de_apiexposures.yaml index 3b27fe79c..399839d01 100644 --- a/api/config/crd/bases/api.cp.ei.telekom.de_apiexposures.yaml +++ b/api/config/crd/bases/api.cp.ei.telekom.de_apiexposures.yaml @@ -154,11 +154,11 @@ spec: format: uri type: string tokenRequest: - description: TokenRequest is the type of token request, - "body" or "header" + description: TokenRequest configures the token endpoint + authentication method (RFC 7591) enum: - - body - - header + - client_secret_basic + - client_secret_post type: string required: - tokenEndpoint diff --git a/api/internal/controller/apiexposure_controller_test.go b/api/internal/controller/apiexposure_controller_test.go index 1070ade09..8369536ec 100644 --- a/api/internal/controller/apiexposure_controller_test.go +++ b/api/internal/controller/apiexposure_controller_test.go @@ -156,7 +156,7 @@ func NewApiExposure(apiBasePath, zoneName string, appName string) *apiv1.ApiExpo M2M: &apiapi.Machine2MachineAuthentication{ ExternalIDP: &apiapi.ExternalIdentityProvider{ TokenEndpoint: "https://example.com/token", - TokenRequest: "header", + TokenRequest: "client_secret_basic", GrantType: "client_credentials", Client: &apiapi.OAuth2ClientCredentials{ ClientId: "client-id", @@ -399,7 +399,7 @@ var _ = Describe("ApiExposure Controller", Ordered, func() { err := k8sClient.Create(ctx, thirdApiExposure) Expect(err).To(HaveOccurred()) Expect(apierrors.IsInvalid(err)).To(BeTrue()) - Expect(err.Error()).To(ContainSubstring("Unsupported value: \"sky\": supported values: \"body\", \"header\"")) + Expect(err.Error()).To(ContainSubstring("Unsupported value: \"sky\": supported values: \"client_secret_basic\", \"client_secret_post\"")) thirdApiExposure.Spec.Security.M2M.ExternalIDP.GrantType = "not_a_valid_grant_type" err = k8sClient.Create(ctx, thirdApiExposure) @@ -413,7 +413,7 @@ var _ = Describe("ApiExposure Controller", Ordered, func() { thirdApiExposure.Spec.Security.M2M = &apiv1.Machine2MachineAuthentication{ ExternalIDP: &apiv1.ExternalIdentityProvider{ TokenEndpoint: "https://example.com/token", - TokenRequest: "header", + TokenRequest: "client_secret_basic", GrantType: "client_credentials", Client: &apiv1.OAuth2ClientCredentials{ ClientId: "team", @@ -439,7 +439,7 @@ var _ = Describe("ApiExposure Controller", Ordered, func() { g.Expect(route.Spec.Security.M2M.Scopes).To(Equal([]string{"team:scope", "api:scope"})) g.Expect(route.Spec.Security.M2M.ExternalIDP.TokenEndpoint).To(Equal("https://example.com/token")) - g.Expect(route.Spec.Security.M2M.ExternalIDP.TokenRequest).To(Equal("header")) + g.Expect(route.Spec.Security.M2M.ExternalIDP.TokenRequest).To(Equal("client_secret_basic")) g.Expect(route.Spec.Security.M2M.ExternalIDP.GrantType).To(Equal("client_credentials")) }, timeout, interval).Should(Succeed()) }) diff --git a/api/internal/controller/apisubscription_controller_ratelimiting_test.go b/api/internal/controller/apisubscription_controller_ratelimiting_test.go index a9b53a56a..d0d79d7e9 100644 --- a/api/internal/controller/apisubscription_controller_ratelimiting_test.go +++ b/api/internal/controller/apisubscription_controller_ratelimiting_test.go @@ -83,7 +83,7 @@ func NewApiExposureWithRateLimit(apiBasePath, zoneName, consumerClientId string, M2M: &apiapi.Machine2MachineAuthentication{ ExternalIDP: &apiapi.ExternalIdentityProvider{ TokenEndpoint: "https://example.com/token", - TokenRequest: "header", + TokenRequest: "client_secret_basic", GrantType: "client_credentials", Client: &apiapi.OAuth2ClientCredentials{ ClientId: "client-id", diff --git a/discovery-server/internal/mapper/apiexposure/out.go b/discovery-server/internal/mapper/apiexposure/out.go index fdfea6c60..d423011d4 100644 --- a/discovery-server/internal/mapper/apiexposure/out.go +++ b/discovery-server/internal/mapper/apiexposure/out.go @@ -14,6 +14,18 @@ import ( "github.com/telekom/controlplane/discovery-server/internal/mapper/status" ) +// tokenRequestCRDToAPI converts CRD tokenRequest values to discovery-server API enum values. +func tokenRequestCRDToAPI(value string) api.OAuth2TokenRequest { + switch strings.ToLower(value) { + case "client_secret_basic": + return api.Header + case "client_secret_post": + return api.Body + default: + return api.OAuth2TokenRequest(value) + } +} + // MapResponse maps an ApiExposure CRD to an ApiExposureResponse. func MapResponse(in *apiv1.ApiExposure) api.ApiExposureResponse { resp := api.ApiExposureResponse{ @@ -82,7 +94,7 @@ func mapSecurity(in *apiv1.ApiExposure, out *api.ApiExposureResponse) { if m2m.ExternalIDP != nil { oauth2 := api.OAuth2{ TokenEndpoint: m2m.ExternalIDP.TokenEndpoint, - TokenRequest: api.OAuth2TokenRequest(m2m.ExternalIDP.TokenRequest), + TokenRequest: tokenRequestCRDToAPI(m2m.ExternalIDP.TokenRequest), GrantType: m2m.ExternalIDP.GrantType, } diff --git a/discovery-server/internal/mapper/apiexposure/out_test.go b/discovery-server/internal/mapper/apiexposure/out_test.go index 35f644546..587e57c1c 100644 --- a/discovery-server/internal/mapper/apiexposure/out_test.go +++ b/discovery-server/internal/mapper/apiexposure/out_test.go @@ -141,7 +141,7 @@ func TestMapSecurity(t *testing.T) { { name: "external idp oauth2", setup: func(in *apiv1.ApiExposure) { - in.Spec.Security = &apiv1.Security{M2M: &apiv1.Machine2MachineAuthentication{ExternalIDP: &apiv1.ExternalIdentityProvider{TokenEndpoint: "https://idp/token", TokenRequest: "body", GrantType: "client_credentials", Client: &apiv1.OAuth2ClientCredentials{ClientId: "cid", ClientSecret: "sec"}}, Scopes: []string{"s1"}}} + in.Spec.Security = &apiv1.Security{M2M: &apiv1.Machine2MachineAuthentication{ExternalIDP: &apiv1.ExternalIdentityProvider{TokenEndpoint: "https://idp/token", TokenRequest: "client_secret_post", GrantType: "client_credentials", Client: &apiv1.OAuth2ClientCredentials{ClientId: "cid", ClientSecret: "sec"}}, Scopes: []string{"s1"}}} }, assert: func(t *testing.T, out api.ApiExposureResponse) { t.Helper() @@ -157,7 +157,7 @@ func TestMapSecurity(t *testing.T) { { name: "external idp oauth2 with basic credentials", setup: func(in *apiv1.ApiExposure) { - in.Spec.Security = &apiv1.Security{M2M: &apiv1.Machine2MachineAuthentication{ExternalIDP: &apiv1.ExternalIdentityProvider{TokenEndpoint: "https://idp/token", TokenRequest: "header", GrantType: "password", Basic: &apiv1.BasicAuthCredentials{Username: "bu", Password: "bp"}}}} + in.Spec.Security = &apiv1.Security{M2M: &apiv1.Machine2MachineAuthentication{ExternalIDP: &apiv1.ExternalIdentityProvider{TokenEndpoint: "https://idp/token", TokenRequest: "client_secret_basic", GrantType: "password", Basic: &apiv1.BasicAuthCredentials{Username: "bu", Password: "bp"}}}} }, assert: func(t *testing.T, out api.ApiExposureResponse) { t.Helper() diff --git a/gateway/api/v1/security_types.go b/gateway/api/v1/security_types.go index ad24fd018..467278792 100644 --- a/gateway/api/v1/security_types.go +++ b/gateway/api/v1/security_types.go @@ -112,9 +112,9 @@ type ExternalIdentityProvider struct { // +kubebuilder:validation:Format=uri TokenEndpoint string `json:"tokenEndpoint"` - // TokenRequest is the type of token request, "body" or "header" + // TokenRequest configures the token endpoint authentication method (RFC 7591) // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=body;header + // +kubebuilder:validation:Enum=client_secret_basic;client_secret_post TokenRequest string `json:"tokenRequest,omitempty"` // GrantType is the grant type for the external IDP authentication // +kubebuilder:validation:Optional diff --git a/gateway/config/crd/bases/gateway.cp.ei.telekom.de_routes.yaml b/gateway/config/crd/bases/gateway.cp.ei.telekom.de_routes.yaml index 0f2d49955..646c7370a 100644 --- a/gateway/config/crd/bases/gateway.cp.ei.telekom.de_routes.yaml +++ b/gateway/config/crd/bases/gateway.cp.ei.telekom.de_routes.yaml @@ -197,11 +197,11 @@ spec: format: uri type: string tokenRequest: - description: TokenRequest is the type of token request, - "body" or "header" + description: TokenRequest configures the token endpoint + authentication method (RFC 7591) enum: - - body - - header + - client_secret_basic + - client_secret_post type: string required: - tokenEndpoint @@ -354,11 +354,11 @@ spec: format: uri type: string tokenRequest: - description: TokenRequest is the type of token - request, "body" or "header" + description: TokenRequest configures the token + endpoint authentication method (RFC 7591) enum: - - body - - header + - client_secret_basic + - client_secret_post type: string required: - tokenEndpoint diff --git a/gateway/internal/controller/route_controller_test.go b/gateway/internal/controller/route_controller_test.go index e582e0f4e..aceb4fdb8 100644 --- a/gateway/internal/controller/route_controller_test.go +++ b/gateway/internal/controller/route_controller_test.go @@ -148,7 +148,7 @@ var _ = Describe("Route Controller", Ordered, func() { err := k8sClient.Create(ctx, route) Expect(err).To(HaveOccurred()) Expect(apierrors.IsInvalid(err)).To(BeTrue()) - Expect(err.Error()).To(ContainSubstring("spec.security.m2m.externalIDP.tokenRequest: Unsupported value: \"sky\": supported values: \"body\", \"header\"")) + Expect(err.Error()).To(ContainSubstring("spec.security.m2m.externalIDP.tokenRequest: Unsupported value: \"sky\": supported values: \"client_secret_basic\", \"client_secret_post\"")) }) It("should not accept a Route with GrantType=\"not_required\"", func() { diff --git a/gateway/internal/features/builder_external_idp_test.go b/gateway/internal/features/builder_external_idp_test.go index 2e291caa6..5f1155452 100644 --- a/gateway/internal/features/builder_external_idp_test.go +++ b/gateway/internal/features/builder_external_idp_test.go @@ -215,7 +215,7 @@ func externalIDPProviderRouteOAuth() *gatewayv1.Route { M2M: &gatewayv1.Machine2MachineAuthentication{ ExternalIDP: &gatewayv1.ExternalIdentityProvider{ TokenEndpoint: "https://example.com/tokenEndpoint", - TokenRequest: "header", + TokenRequest: "client_secret_basic", GrantType: "client_credentials", Client: &gatewayv1.OAuth2ClientCredentials{ ClientId: "gateway", @@ -243,7 +243,7 @@ func externalIDPProviderRouteBasic() *gatewayv1.Route { M2M: &gatewayv1.Machine2MachineAuthentication{ ExternalIDP: &gatewayv1.ExternalIdentityProvider{ TokenEndpoint: "https://example.com/tokenEndpoint", - TokenRequest: "header", + TokenRequest: "client_secret_basic", GrantType: "password", Basic: &gatewayv1.BasicAuthCredentials{ Username: "user", @@ -271,7 +271,7 @@ func externalIDPProviderRouteOAuthJwt() *gatewayv1.Route { M2M: &gatewayv1.Machine2MachineAuthentication{ ExternalIDP: &gatewayv1.ExternalIdentityProvider{ TokenEndpoint: "https://example.com/tokenEndpoint", - TokenRequest: "header", + TokenRequest: "client_secret_basic", GrantType: "client_credentials", Client: &gatewayv1.OAuth2ClientCredentials{ ClientId: "ClientId", diff --git a/gateway/internal/features/feature/external_idp.go b/gateway/internal/features/feature/external_idp.go index fb7393f3b..a1a58ad7c 100644 --- a/gateway/internal/features/feature/external_idp.go +++ b/gateway/internal/features/feature/external_idp.go @@ -6,6 +6,7 @@ package feature import ( "context" + "fmt" "strings" "github.com/pkg/errors" @@ -146,7 +147,11 @@ func extendOauth(ctx context.Context, in plugin.OauthCredentials, providerSettin in.Scopes = strings.Join(scopes, " ") } - in.TokenRequest = providerSettings.TokenRequest + tokenRequest, err := tokenRequestToJumper(providerSettings.TokenRequest) + if err != nil { + return in, err + } + in.TokenRequest = tokenRequest in.GrantType = providerSettings.GrantType return in, nil @@ -182,3 +187,15 @@ func extendBasic(ctx context.Context, in plugin.OauthCredentials, providerSettin return in, nil } + +// tokenRequestToJumper converts CRD tokenRequest values to the values expected by the Jumper plugin. +func tokenRequestToJumper(value string) (string, error) { + switch strings.ToLower(value) { + case "client_secret_basic": + return "header", nil + case "client_secret_post": + return "body", nil + default: + return "", fmt.Errorf("unsupported tokenRequest value %q", value) + } +} diff --git a/rover-server/internal/mapper/rover/in/__snapshots__/exposure_security_test.snap b/rover-server/internal/mapper/rover/in/__snapshots__/exposure_security_test.snap index 94a0af11a..c24660c2c 100755 --- a/rover-server/internal/mapper/rover/in/__snapshots__/exposure_security_test.snap +++ b/rover-server/internal/mapper/rover/in/__snapshots__/exposure_security_test.snap @@ -14,7 +14,7 @@ M2M: &v1.Machine2MachineAuthentication{ ExternalIDP: &v1.ExternalIdentityProvider{ TokenEndpoint: "https://test.com/token", - TokenRequest: "basic", + TokenRequest: "client_secret_basic", GrantType: "client_credentials", Basic: (*v1.BasicAuthCredentials)(nil), Client: &v1.OAuth2ClientCredentials{ClientId:"client-id", ClientSecret:"client-secret", ClientKey:"client-key"}, @@ -30,7 +30,7 @@ M2M: &v1.Machine2MachineAuthentication{ ExternalIDP: &v1.ExternalIdentityProvider{ TokenEndpoint: "https://test.com/token", - TokenRequest: "basic", + TokenRequest: "client_secret_basic", GrantType: "password", Basic: &v1.BasicAuthCredentials{Username:"testuser", Password:"testpass"}, Client: (*v1.OAuth2ClientCredentials)(nil), diff --git a/rover-server/internal/mapper/rover/in/exposure.go b/rover-server/internal/mapper/rover/in/exposure.go index 504ca0923..7396010ae 100644 --- a/rover-server/internal/mapper/rover/in/exposure.go +++ b/rover-server/internal/mapper/rover/in/exposure.go @@ -17,6 +17,20 @@ import ( "github.com/telekom/controlplane/rover-server/internal/api" ) +// oauth2TokenRequestToCRD maps API tokenRequest values to CRD tokenRequest values. +var oauth2TokenRequestToCRD = map[string]string{ + "body": "client_secret_post", + "header": "client_secret_basic", + "basic": "client_secret_basic", +} + +func tokenRequestAPIToCRD(value string) string { + if mapped, ok := oauth2TokenRequestToCRD[strings.ToLower(value)]; ok { + return mapped + } + return value +} + func mapExposure(in *api.Exposure, out *roverv1.Exposure) error { expType, err := in.Discriminator() if err != nil { @@ -134,7 +148,7 @@ func mapExposureSecurity(in api.ApiExposure, out *roverv1.ApiExposure) { // external-idp m2mSecurity.ExternalIDP = &roverv1.ExternalIdentityProvider{ TokenEndpoint: oauth2.TokenEndpoint, - TokenRequest: string(oauth2.TokenRequest), + TokenRequest: tokenRequestAPIToCRD(string(oauth2.TokenRequest)), GrantType: strings.ToLower(string(oauth2.GrantType)), } if oauth2.ClientId != "" { diff --git a/rover-server/internal/mapper/rover/in/exposure_security_test.go b/rover-server/internal/mapper/rover/in/exposure_security_test.go index 2cdfc255c..abf25755f 100644 --- a/rover-server/internal/mapper/rover/in/exposure_security_test.go +++ b/rover-server/internal/mapper/rover/in/exposure_security_test.go @@ -69,7 +69,7 @@ var _ = Describe("Exposure Security Mapper", func() { Expect(output.Security.M2M).ToNot(BeNil()) Expect(output.Security.M2M.ExternalIDP).ToNot(BeNil()) Expect(output.Security.M2M.ExternalIDP.TokenEndpoint).To(Equal("https://test.com/token")) - Expect(output.Security.M2M.ExternalIDP.TokenRequest).To(Equal("basic")) + Expect(output.Security.M2M.ExternalIDP.TokenRequest).To(Equal("client_secret_basic")) Expect(output.Security.M2M.ExternalIDP.GrantType).To(Equal("client_credentials")) Expect(output.Security.M2M.ExternalIDP.Client).ToNot(BeNil()) Expect(output.Security.M2M.ExternalIDP.Client.ClientId).To(Equal("client-id")) diff --git a/rover-server/internal/mapper/rover/in/rover.go b/rover-server/internal/mapper/rover/in/rover.go index 940b240bd..6ded877a2 100644 --- a/rover-server/internal/mapper/rover/in/rover.go +++ b/rover-server/internal/mapper/rover/in/rover.go @@ -147,8 +147,8 @@ func mapPermissions(in *api.Rover, out *roverv1.Rover) error { // clientAuthMethodToCRD maps rover-server API enum values to rover CRD tokenRequest values. var clientAuthMethodToCRD = map[api.AuthenticationClientAuthMethod]string{ - api.BASIC: "header", - api.POST: "body", + api.BASIC: "client_secret_basic", + api.POST: "client_secret_post", } func mapAuthentication(in *api.Rover, out *roverv1.Rover) { diff --git a/rover-server/internal/mapper/rover/in/rover_test.go b/rover-server/internal/mapper/rover/in/rover_test.go index 5bd5222b5..16862b437 100644 --- a/rover-server/internal/mapper/rover/in/rover_test.go +++ b/rover-server/internal/mapper/rover/in/rover_test.go @@ -141,7 +141,7 @@ var _ = Describe("Rover Mapper", func() { }) Context("MapAuthentication", func() { - It("must map BASIC to header in CRD", func() { + It("must map BASIC to client_secret_basic in CRD", func() { input := &api.Rover{ Zone: "zone", Authentication: api.Authentication{ @@ -155,10 +155,10 @@ var _ = Describe("Rover Mapper", func() { Expect(err).ToNot(HaveOccurred()) Expect(output.Spec.Authentication).ToNot(BeNil()) Expect(output.Spec.Authentication.M2M).ToNot(BeNil()) - Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("header")) + Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("client_secret_basic")) }) - It("must map POST to body in CRD", func() { + It("must map POST to client_secret_post in CRD", func() { input := &api.Rover{ Zone: "zone", Authentication: api.Authentication{ @@ -172,7 +172,7 @@ var _ = Describe("Rover Mapper", func() { Expect(err).ToNot(HaveOccurred()) Expect(output.Spec.Authentication).ToNot(BeNil()) Expect(output.Spec.Authentication.M2M).ToNot(BeNil()) - Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("body")) + Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("client_secret_post")) }) It("must not set authentication when clientAuthMethod is empty", func() { @@ -187,7 +187,7 @@ var _ = Describe("Rover Mapper", func() { Expect(output.Spec.Authentication).To(BeNil()) }) - It("must fuzzy-match 'basic' to header in CRD", func() { + It("must fuzzy-match 'basic' to client_secret_basic in CRD", func() { input := &api.Rover{ Zone: "zone", Authentication: api.Authentication{ @@ -201,10 +201,10 @@ var _ = Describe("Rover Mapper", func() { Expect(err).ToNot(HaveOccurred()) Expect(output.Spec.Authentication).ToNot(BeNil()) Expect(output.Spec.Authentication.M2M).ToNot(BeNil()) - Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("header")) + Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("client_secret_basic")) }) - It("must fuzzy-match 'body' to body in CRD", func() { + It("must fuzzy-match 'body' to client_secret_post in CRD", func() { input := &api.Rover{ Zone: "zone", Authentication: api.Authentication{ @@ -218,7 +218,7 @@ var _ = Describe("Rover Mapper", func() { Expect(err).ToNot(HaveOccurred()) Expect(output.Spec.Authentication).ToNot(BeNil()) Expect(output.Spec.Authentication.M2M).ToNot(BeNil()) - Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("body")) + Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("client_secret_post")) }) }) diff --git a/rover-server/internal/mapper/rover/out/__snapshots__/exposure_security_test.snap b/rover-server/internal/mapper/rover/out/__snapshots__/exposure_security_test.snap index e8e4328ed..e35994fc8 100755 --- a/rover-server/internal/mapper/rover/out/__snapshots__/exposure_security_test.snap +++ b/rover-server/internal/mapper/rover/out/__snapshots__/exposure_security_test.snap @@ -13,7 +13,7 @@ api.Oauth2{ RefreshToken: "", Scopes: nil, TokenEndpoint: "https://test.com/token", - TokenRequest: "basic", + TokenRequest: "header", Type: "oauth2", Username: "", } diff --git a/rover-server/internal/mapper/rover/out/exposure.go b/rover-server/internal/mapper/rover/out/exposure.go index 733b7714a..e464216d8 100644 --- a/rover-server/internal/mapper/rover/out/exposure.go +++ b/rover-server/internal/mapper/rover/out/exposure.go @@ -14,6 +14,19 @@ import ( "github.com/telekom/controlplane/rover-server/internal/api" ) +// oauth2TokenRequestCRDToAPI maps CRD tokenRequest values to API Oauth2TokenRequest values. +var oauth2TokenRequestCRDToAPI = map[string]api.Oauth2TokenRequest{ + "client_secret_basic": api.Header, + "client_secret_post": api.Body, +} + +func tokenRequestCRDToAPI(value string) api.Oauth2TokenRequest { + if mapped, ok := oauth2TokenRequestCRDToAPI[strings.ToLower(value)]; ok { + return mapped + } + return api.Oauth2TokenRequest(value) +} + func mapExposure(in *roverv1.Exposure, out *api.Exposure) error { if in.Api != nil { if err := out.FromApiExposure(mapApiExposure(in.Api)); err != nil { @@ -169,7 +182,7 @@ func mapExposureSecurity(in *roverv1.ApiExposure, out *api.ApiExposure) { if m2m.ExternalIDP != nil { oauth2 := api.Oauth2{ TokenEndpoint: m2m.ExternalIDP.TokenEndpoint, - TokenRequest: api.Oauth2TokenRequest(m2m.ExternalIDP.TokenRequest), + TokenRequest: tokenRequestCRDToAPI(m2m.ExternalIDP.TokenRequest), } if grantType := api.GrantType(m2m.ExternalIDP.GrantType); grantType != "" { diff --git a/rover-server/internal/mapper/rover/out/exposure_security_test.go b/rover-server/internal/mapper/rover/out/exposure_security_test.go index 1633f62ea..f6c420131 100644 --- a/rover-server/internal/mapper/rover/out/exposure_security_test.go +++ b/rover-server/internal/mapper/rover/out/exposure_security_test.go @@ -51,7 +51,7 @@ var _ = Describe("Exposure Security Mapper (Out)", func() { M2M: &roverv1.Machine2MachineAuthentication{ ExternalIDP: &roverv1.ExternalIdentityProvider{ TokenEndpoint: "https://test.com/token", - TokenRequest: "basic", + TokenRequest: "client_secret_basic", GrantType: "client_credentials", Client: &roverv1.OAuth2ClientCredentials{ ClientId: "client-id", @@ -73,7 +73,7 @@ var _ = Describe("Exposure Security Mapper (Out)", func() { oauth2, err := output.Security.AsOauth2() Expect(err).To(BeNil()) Expect(oauth2.TokenEndpoint).To(Equal("https://test.com/token")) - Expect(oauth2.TokenRequest).To(Equal(api.Oauth2TokenRequest("basic"))) + Expect(oauth2.TokenRequest).To(Equal(api.Oauth2TokenRequest("header"))) Expect(oauth2.GrantType).To(Equal(api.GrantType("client_credentials"))) Expect(oauth2.ClientId).To(Equal("client-id")) snaps.MatchSnapshot(GinkgoT(), oauth2) diff --git a/rover-server/internal/mapper/rover/out/rover.go b/rover-server/internal/mapper/rover/out/rover.go index bba814a43..bb3c8760a 100644 --- a/rover-server/internal/mapper/rover/out/rover.go +++ b/rover-server/internal/mapper/rover/out/rover.go @@ -49,8 +49,8 @@ func MapRover(in *roverv1.Rover, out *api.Rover) error { // tokenRequestToAPI maps rover CRD tokenRequest values to rover-server API enum values. var tokenRequestToAPI = map[string]api.AuthenticationClientAuthMethod{ - "header": api.BASIC, - "body": api.POST, + "client_secret_basic": api.BASIC, + "client_secret_post": api.POST, } func mapAuthentication(in *roverv1.Rover, out *api.Rover) { diff --git a/rover-server/internal/mapper/rover/out/rover_test.go b/rover-server/internal/mapper/rover/out/rover_test.go index 09060a5e4..e0060c179 100644 --- a/rover-server/internal/mapper/rover/out/rover_test.go +++ b/rover-server/internal/mapper/rover/out/rover_test.go @@ -61,11 +61,11 @@ var _ = Describe("Rover Mapper", func() { }) Context("MapAuthentication", func() { - It("must map header from CRD to BASIC in API", func() { + It("must map client_secret_basic from CRD to BASIC in API", func() { input := rover.DeepCopy() input.Spec.Authentication = &roverv1.RoverAuthentication{ M2M: &roverv1.RoverM2MAuthentication{ - TokenRequest: "header", + TokenRequest: "client_secret_basic", }, } output := &api.Rover{} @@ -76,11 +76,11 @@ var _ = Describe("Rover Mapper", func() { Expect(output.Authentication.ClientAuthMethod).To(Equal(api.BASIC)) }) - It("must map body from CRD to POST in API", func() { + It("must map client_secret_post from CRD to POST in API", func() { input := rover.DeepCopy() input.Spec.Authentication = &roverv1.RoverAuthentication{ M2M: &roverv1.RoverM2MAuthentication{ - TokenRequest: "body", + TokenRequest: "client_secret_post", }, } output := &api.Rover{} diff --git a/rover/api/v1/rover_types.go b/rover/api/v1/rover_types.go index 568f7fd58..159fe5880 100644 --- a/rover/api/v1/rover_types.go +++ b/rover/api/v1/rover_types.go @@ -189,12 +189,12 @@ type RoverAuthentication struct { // RoverM2MAuthentication defines the M2M authentication settings type RoverM2MAuthentication struct { - // TokenRequest configures the client authentication method, according to RFC 6749 + // TokenRequest configures the token endpoint authentication method (RFC 7591) // This feature is currently only documented but not parsed towards the application and identity domain as it is still in discussion whether // this should will be enforced for IDPs. // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=body;header - // +kubebuilder:default=header + // +kubebuilder:validation:Enum=client_secret_basic;client_secret_post + // +kubebuilder:default=client_secret_basic TokenRequest string `json:"tokenRequest,omitempty"` } diff --git a/rover/api/v1/security_types.go b/rover/api/v1/security_types.go index 1315f9027..35d18474a 100644 --- a/rover/api/v1/security_types.go +++ b/rover/api/v1/security_types.go @@ -63,9 +63,9 @@ type ExternalIdentityProvider struct { // +kubebuilder:validation:Format=uri TokenEndpoint string `json:"tokenEndpoint"` - // TokenRequest is the type of token request, "body" or "header" + // TokenRequest configures the token endpoint authentication method (RFC 7591) // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=body;header + // +kubebuilder:validation:Enum=client_secret_basic;client_secret_post TokenRequest string `json:"tokenRequest,omitempty"` // GrantType defines the OAuth2 grant type to use for the token request diff --git a/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml b/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml index 685af5c63..215e9fbcd 100644 --- a/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml +++ b/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml @@ -58,14 +58,14 @@ spec: for the application properties: tokenRequest: - default: header + default: client_secret_basic description: |- - TokenRequest configures the client authentication method, according to RFC 6749 + TokenRequest configures the token endpoint authentication method (RFC 7591) This feature is currently only documented but not parsed towards the application and identity domain as it is still in discussion whether this should will be enforced for IDPs. enum: - - body - - header + - client_secret_basic + - client_secret_post type: string type: object type: object @@ -214,11 +214,11 @@ spec: format: uri type: string tokenRequest: - description: TokenRequest is the type of token - request, "body" or "header" + description: TokenRequest configures the token + endpoint authentication method (RFC 7591) enum: - - body - - header + - client_secret_basic + - client_secret_post type: string required: - tokenEndpoint diff --git a/rover/internal/controller/rover_controller_test.go b/rover/internal/controller/rover_controller_test.go index ea3e7630c..78fe49971 100644 --- a/rover/internal/controller/rover_controller_test.go +++ b/rover/internal/controller/rover_controller_test.go @@ -472,7 +472,7 @@ var _ = Describe("Rover Controller", Ordered, func() { M2M: &roverv1.Machine2MachineAuthentication{ ExternalIDP: &roverv1.ExternalIdentityProvider{ TokenEndpoint: "https://idp.example.com/token", - TokenRequest: "header", + TokenRequest: "client_secret_basic", GrantType: "client_credentials", Basic: nil, Client: &roverv1.OAuth2ClientCredentials{ @@ -524,7 +524,7 @@ var _ = Describe("Rover Controller", Ordered, func() { g.Expect(apiExposure.Spec.Security.M2M.ExternalIDP.Client.ClientId).To(Equal("clientID")) g.Expect(apiExposure.Spec.Security.M2M.ExternalIDP.Client.ClientSecret).To(Equal("******")) g.Expect(apiExposure.Spec.Security.M2M.Scopes[0]).To(Equal("eIDP:scope")) - g.Expect(apiExposure.Spec.Security.M2M.ExternalIDP.TokenRequest).To(Equal("header")) + g.Expect(apiExposure.Spec.Security.M2M.ExternalIDP.TokenRequest).To(Equal("client_secret_basic")) g.Expect(apiExposure.Spec.Security.M2M.ExternalIDP.TokenEndpoint).To(Equal("https://idp.example.com/token")) g.Expect(apiExposure.Spec.Security.M2M.ExternalIDP.GrantType).To(Equal("client_credentials")) }, timeout, interval).Should(Succeed()) From fe7df33b6a556f4e9fe1e43e2c5c2ae0b8bc17dc Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Tue, 28 Apr 2026 14:08:54 +0200 Subject: [PATCH 11/16] refactor: replace hardcoded tokenRequest values with constants from rovermapper --- api/internal/controller/apiexposure_controller_test.go | 1 + rover-server/internal/mapper/rover/in/exposure.go | 7 ++++--- rover-server/internal/mapper/rover/in/fuzzy_match.go | 10 ++++++---- rover-server/internal/mapper/rover/in/rover.go | 5 +++-- rover-server/internal/mapper/rover/out/exposure.go | 5 +++-- rover-server/internal/mapper/rover/out/rover.go | 5 +++-- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/api/internal/controller/apiexposure_controller_test.go b/api/internal/controller/apiexposure_controller_test.go index 8369536ec..912082815 100644 --- a/api/internal/controller/apiexposure_controller_test.go +++ b/api/internal/controller/apiexposure_controller_test.go @@ -6,6 +6,7 @@ package controller import ( "fmt" + "github.com/telekom/controlplane/api/internal/handler/util" applicationapi "github.com/telekom/controlplane/application/api/v1" diff --git a/rover-server/internal/mapper/rover/in/exposure.go b/rover-server/internal/mapper/rover/in/exposure.go index 7396010ae..c6016af5b 100644 --- a/rover-server/internal/mapper/rover/in/exposure.go +++ b/rover-server/internal/mapper/rover/in/exposure.go @@ -15,13 +15,14 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "github.com/telekom/controlplane/rover-server/internal/api" + rovermapper "github.com/telekom/controlplane/rover-server/internal/mapper/rover" ) // oauth2TokenRequestToCRD maps API tokenRequest values to CRD tokenRequest values. var oauth2TokenRequestToCRD = map[string]string{ - "body": "client_secret_post", - "header": "client_secret_basic", - "basic": "client_secret_basic", + "body": rovermapper.TokenRequestClientSecretPost, + "header": rovermapper.TokenRequestClientSecretBasic, + "basic": rovermapper.TokenRequestClientSecretBasic, } func tokenRequestAPIToCRD(value string) string { diff --git a/rover-server/internal/mapper/rover/in/fuzzy_match.go b/rover-server/internal/mapper/rover/in/fuzzy_match.go index a15dee93e..24eba4700 100644 --- a/rover-server/internal/mapper/rover/in/fuzzy_match.go +++ b/rover-server/internal/mapper/rover/in/fuzzy_match.go @@ -5,6 +5,8 @@ package in import ( + "strings" + "github.com/telekom/controlplane/rover-server/internal/api" roverv1 "github.com/telekom/controlplane/rover/api/v1" ) @@ -47,12 +49,12 @@ func FuzzyMatchEventResponseFilterMode(in string) roverv1.EventResponseFilterMod // FuzzyMatchClientAuthMethod performs a fuzzy match on the input string to determine the AuthenticationClientAuthMethod. func FuzzyMatchClientAuthMethod(in string) api.AuthenticationClientAuthMethod { - switch in { - case "basic", "Basic", "BASIC": + switch strings.ToLower(in) { + case "basic": return api.BASIC - case "body", "Body", "BODY", "post", "Post", "POST": + case "body", "post": return api.POST - case "none", "None", "NONE": + case "none": return api.NONE default: return api.AuthenticationClientAuthMethod(in) diff --git a/rover-server/internal/mapper/rover/in/rover.go b/rover-server/internal/mapper/rover/in/rover.go index 6ded877a2..078390129 100644 --- a/rover-server/internal/mapper/rover/in/rover.go +++ b/rover-server/internal/mapper/rover/in/rover.go @@ -13,6 +13,7 @@ import ( "github.com/telekom/controlplane/rover-server/internal/api" "github.com/telekom/controlplane/rover-server/internal/mapper" + rovermapper "github.com/telekom/controlplane/rover-server/internal/mapper/rover" ) func MapRequest(in *api.RoverUpdateRequest, id mapper.ResourceIdInfo) (res *roverv1.Rover, err error) { @@ -147,8 +148,8 @@ func mapPermissions(in *api.Rover, out *roverv1.Rover) error { // clientAuthMethodToCRD maps rover-server API enum values to rover CRD tokenRequest values. var clientAuthMethodToCRD = map[api.AuthenticationClientAuthMethod]string{ - api.BASIC: "client_secret_basic", - api.POST: "client_secret_post", + api.BASIC: rovermapper.TokenRequestClientSecretBasic, + api.POST: rovermapper.TokenRequestClientSecretPost, } func mapAuthentication(in *api.Rover, out *roverv1.Rover) { diff --git a/rover-server/internal/mapper/rover/out/exposure.go b/rover-server/internal/mapper/rover/out/exposure.go index e464216d8..eaebdf4e9 100644 --- a/rover-server/internal/mapper/rover/out/exposure.go +++ b/rover-server/internal/mapper/rover/out/exposure.go @@ -12,12 +12,13 @@ import ( roverv1 "github.com/telekom/controlplane/rover/api/v1" "github.com/telekom/controlplane/rover-server/internal/api" + rovermapper "github.com/telekom/controlplane/rover-server/internal/mapper/rover" ) // oauth2TokenRequestCRDToAPI maps CRD tokenRequest values to API Oauth2TokenRequest values. var oauth2TokenRequestCRDToAPI = map[string]api.Oauth2TokenRequest{ - "client_secret_basic": api.Header, - "client_secret_post": api.Body, + rovermapper.TokenRequestClientSecretBasic: api.Header, + rovermapper.TokenRequestClientSecretPost: api.Body, } func tokenRequestCRDToAPI(value string) api.Oauth2TokenRequest { diff --git a/rover-server/internal/mapper/rover/out/rover.go b/rover-server/internal/mapper/rover/out/rover.go index bb3c8760a..792b6fc59 100644 --- a/rover-server/internal/mapper/rover/out/rover.go +++ b/rover-server/internal/mapper/rover/out/rover.go @@ -12,6 +12,7 @@ import ( "github.com/telekom/controlplane/rover-server/internal/api" "github.com/telekom/controlplane/rover-server/internal/mapper" + rovermapper "github.com/telekom/controlplane/rover-server/internal/mapper/rover" "github.com/telekom/controlplane/rover-server/internal/mapper/status" "github.com/telekom/controlplane/rover-server/pkg/store" ) @@ -49,8 +50,8 @@ func MapRover(in *roverv1.Rover, out *api.Rover) error { // tokenRequestToAPI maps rover CRD tokenRequest values to rover-server API enum values. var tokenRequestToAPI = map[string]api.AuthenticationClientAuthMethod{ - "client_secret_basic": api.BASIC, - "client_secret_post": api.POST, + rovermapper.TokenRequestClientSecretBasic: api.BASIC, + rovermapper.TokenRequestClientSecretPost: api.POST, } func mapAuthentication(in *roverv1.Rover, out *api.Rover) { From 5e931dd127153716fef165dc34c035be70f87e22 Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Tue, 28 Apr 2026 14:12:05 +0200 Subject: [PATCH 12/16] feat(tokenRequest): add initial tokenRequest values for RFC 7591 client authentication methods Co-authored-by: Copilot --- rover-server/internal/mapper/rover/token_request.go | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 rover-server/internal/mapper/rover/token_request.go diff --git a/rover-server/internal/mapper/rover/token_request.go b/rover-server/internal/mapper/rover/token_request.go new file mode 100644 index 000000000..55baba1a6 --- /dev/null +++ b/rover-server/internal/mapper/rover/token_request.go @@ -0,0 +1,11 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package rover + +// CRD tokenRequest values (RFC 7591 client authentication methods). +const ( + TokenRequestClientSecretBasic = "client_secret_basic" + TokenRequestClientSecretPost = "client_secret_post" +) From 4484ab40f2cf73149201747937b5eec353774b06 Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Tue, 5 May 2026 10:38:32 +0200 Subject: [PATCH 13/16] feat(auth): normalize clientAuthMethod values in PatchAuthentication function --- rover-ctl/pkg/handlers/v0/rover.go | 28 ++++++++++++++++++++--- rover-ctl/pkg/handlers/v0/rover_test.go | 30 +++++++++++++++++++++---- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/rover-ctl/pkg/handlers/v0/rover.go b/rover-ctl/pkg/handlers/v0/rover.go index 8172660a5..f9dd3aa32 100644 --- a/rover-ctl/pkg/handlers/v0/rover.go +++ b/rover-ctl/pkg/handlers/v0/rover.go @@ -9,6 +9,7 @@ import ( "encoding/json" "maps" "net/http" + "strings" "github.com/pkg/errors" "github.com/telekom/controlplane/rover-ctl/pkg/handlers/common" @@ -75,8 +76,8 @@ func PatchRoverRequest(ctx context.Context, obj types.Object) error { // PatchAuthentication restructures spec.authentication.m2m.clientAuthMethod // into spec.authentication.clientAuthMethod for the rover-server API format. -// Value normalization (e.g. basic→BASIC, body→POST) is handled server-side -// by FuzzyMatchClientAuthMethod. +// It also normalizes "BODY"/"body" to "POST" since the server schema only accepts +// NONE, POST, BASIC. func PatchAuthentication(spec map[string]any) { auth, exists := spec["authentication"] if !exists { @@ -102,7 +103,28 @@ func PatchAuthentication(spec map[string]any) { } spec["authentication"] = map[string]any{ - "clientAuthMethod": clientAuthMethod, + "clientAuthMethod": normalizeClientAuthMethod(clientAuthMethod), + } +} + +// normalizeClientAuthMethod maps user-friendly aliases to the API enum values. +// "BODY"/"body" is treated as "POST" per RFC 6749. +func normalizeClientAuthMethod(value any) any { + s, ok := value.(string) + if !ok { + return value + } + switch strings.ToUpper(s) { + case "BODY": + return "POST" + case "BASIC": + return "BASIC" + case "NONE": + return "NONE" + case "POST": + return "POST" + default: + return value } } diff --git a/rover-ctl/pkg/handlers/v0/rover_test.go b/rover-ctl/pkg/handlers/v0/rover_test.go index 73f276128..364c4cf64 100644 --- a/rover-ctl/pkg/handlers/v0/rover_test.go +++ b/rover-ctl/pkg/handlers/v0/rover_test.go @@ -382,7 +382,7 @@ var _ = Describe("Rover Handler", func() { }) Describe("PatchAuthentication", func() { - It("should pass through 'basic' as-is in authentication.clientAuthMethod", func() { + It("should normalize 'basic' to 'BASIC' in authentication.clientAuthMethod", func() { obj := &types.UnstructuredObject{ Content: map[string]any{ "spec": map[string]any{ @@ -401,10 +401,10 @@ var _ = Describe("Rover Handler", func() { content := obj.GetContent() auth, ok := content["authentication"].(map[string]any) Expect(ok).To(BeTrue()) - Expect(auth["clientAuthMethod"]).To(Equal("basic")) + Expect(auth["clientAuthMethod"]).To(Equal("BASIC")) }) - It("should pass through 'body' as-is in authentication.clientAuthMethod", func() { + It("should normalize 'body' to 'POST' in authentication.clientAuthMethod", func() { obj := &types.UnstructuredObject{ Content: map[string]any{ "spec": map[string]any{ @@ -423,7 +423,29 @@ var _ = Describe("Rover Handler", func() { content := obj.GetContent() auth, ok := content["authentication"].(map[string]any) Expect(ok).To(BeTrue()) - Expect(auth["clientAuthMethod"]).To(Equal("body")) + Expect(auth["clientAuthMethod"]).To(Equal("POST")) + }) + + It("should normalize 'BODY' to 'POST' in authentication.clientAuthMethod", func() { + obj := &types.UnstructuredObject{ + Content: map[string]any{ + "spec": map[string]any{ + "authentication": map[string]any{ + "m2m": map[string]any{ + "clientAuthMethod": "BODY", + }, + }, + }, + }, + } + + err := v0.PatchRoverRequest(context.Background(), obj) + Expect(err).NotTo(HaveOccurred()) + + content := obj.GetContent() + auth, ok := content["authentication"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(auth["clientAuthMethod"]).To(Equal("POST")) }) It("should not add authentication when it is missing", func() { From 99d318d2c6f76af6f47d02b36c29fcfc4ceff3ef Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Wed, 13 May 2026 13:49:50 +0200 Subject: [PATCH 14/16] chore: solve some local issues --- .../controller/__snapshots__/suite_controller_test.snap | 4 ++-- rover-server/internal/mapper/rover/out/rover_test.go | 2 -- .../internal/mapper/status/__snapshots__/status_test.snap | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/rover-server/internal/controller/__snapshots__/suite_controller_test.snap b/rover-server/internal/controller/__snapshots__/suite_controller_test.snap index 88b9ad5a1..1415b5b5e 100644 --- a/rover-server/internal/controller/__snapshots__/suite_controller_test.snap +++ b/rover-server/internal/controller/__snapshots__/suite_controller_test.snap @@ -449,7 +449,7 @@ [Rover Controller Get rover status should return the status of a rover successfully - 1] { - "createdAt": "2025-09-18T10:39:44+02:00", + "createdAt": "2025-09-18T08:39:44Z", "errors": [ { "cause": "NoApproval", @@ -464,7 +464,7 @@ } ], "overallStatus": "blocked", - "processedAt": "2025-10-08T09:16:40+02:00", + "processedAt": "2025-10-08T07:16:40Z", "processingState": "done", "state": "blocked" } diff --git a/rover-server/internal/mapper/rover/out/rover_test.go b/rover-server/internal/mapper/rover/out/rover_test.go index 66eaebaa6..8916b083a 100644 --- a/rover-server/internal/mapper/rover/out/rover_test.go +++ b/rover-server/internal/mapper/rover/out/rover_test.go @@ -8,8 +8,6 @@ import ( "github.com/gkampitakis/go-snaps/snaps" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - roverv1 "github.com/telekom/controlplane/rover/api/v1" - "github.com/telekom/controlplane/rover-server/internal/api" roverv1 "github.com/telekom/controlplane/rover/api/v1" ) diff --git a/rover-server/internal/mapper/status/__snapshots__/status_test.snap b/rover-server/internal/mapper/status/__snapshots__/status_test.snap index 3f445fede..5e2982038 100644 --- a/rover-server/internal/mapper/status/__snapshots__/status_test.snap +++ b/rover-server/internal/mapper/status/__snapshots__/status_test.snap @@ -19,7 +19,7 @@ [Rover Status Mapper MapRoverResponse must map rover response correctly - 1] { - "createdAt": "2025-09-18T10:39:44+02:00", + "createdAt": "2025-09-18T08:39:44Z", "errors": [ { "cause": "NoApproval", @@ -34,7 +34,7 @@ } ], "overallStatus": "blocked", - "processedAt": "2025-10-08T09:16:40+02:00", + "processedAt": "2025-10-08T07:16:40Z", "processingState": "done", "state": "blocked" } From 5dcb703af61dcf226b0ef2abc7f6f46d1f44713a Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Wed, 13 May 2026 17:15:58 +0200 Subject: [PATCH 15/16] refactor: introduce enum --- api/api/v1/security_types.go | 12 ++++++++++-- .../controller/apiexposure_controller_test.go | 8 ++++---- .../apisubscription_controller_ratelimiting_test.go | 2 +- api/internal/handler/util/route_util.go | 2 +- discovery-server/internal/mapper/apiexposure/out.go | 8 ++++---- .../internal/mapper/apiexposure/out_test.go | 4 ++-- gateway/api/v1/security_types.go | 12 ++++++++++-- .../internal/controller/route_controller_test.go | 2 +- .../internal/features/builder_external_idp_test.go | 6 +++--- gateway/internal/features/feature/external_idp.go | 8 ++++---- rover-server/internal/mapper/rover/in/exposure.go | 13 ++++++------- .../mapper/rover/in/exposure_security_test.go | 2 +- rover-server/internal/mapper/rover/in/rover.go | 7 +++---- rover-server/internal/mapper/rover/in/rover_test.go | 8 ++++---- rover-server/internal/mapper/rover/out/exposure.go | 11 +++++------ .../mapper/rover/out/exposure_security_test.go | 2 +- rover-server/internal/mapper/rover/out/rover.go | 7 +++---- .../internal/mapper/rover/out/rover_test.go | 4 ++-- rover-server/internal/mapper/rover/token_request.go | 11 ----------- rover/api/v1/rover_types.go | 3 +-- rover/api/v1/security_types.go | 12 ++++++++++-- rover/internal/controller/rover_controller_test.go | 4 ++-- rover/internal/handler/rover/api/exposure.go | 2 +- 23 files changed, 79 insertions(+), 71 deletions(-) delete mode 100644 rover-server/internal/mapper/rover/token_request.go diff --git a/api/api/v1/security_types.go b/api/api/v1/security_types.go index cc73ab590..29f161de2 100644 --- a/api/api/v1/security_types.go +++ b/api/api/v1/security_types.go @@ -4,6 +4,15 @@ package v1 +// TokenRequestMethod defines the token endpoint authentication method (RFC 7591). +// +kubebuilder:validation:Enum=client_secret_basic;client_secret_post +type TokenRequestMethod string + +const ( + TokenRequestClientSecretBasic TokenRequestMethod = "client_secret_basic" + TokenRequestClientSecretPost TokenRequestMethod = "client_secret_post" +) + // Security defines the security configuration for the Rover // Security is optional, but if provided, exactly one of m2m or h2m must be set type Security struct { @@ -66,8 +75,7 @@ type ExternalIdentityProvider struct { // TokenRequest configures the token endpoint authentication method (RFC 7591) // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=client_secret_basic;client_secret_post - TokenRequest string `json:"tokenRequest,omitempty"` + TokenRequest TokenRequestMethod `json:"tokenRequest,omitempty"` // GrantType defines the OAuth2 grant type to use for the token request // +kubebuilder:validation:Optional diff --git a/api/internal/controller/apiexposure_controller_test.go b/api/internal/controller/apiexposure_controller_test.go index 912082815..187e1ef22 100644 --- a/api/internal/controller/apiexposure_controller_test.go +++ b/api/internal/controller/apiexposure_controller_test.go @@ -157,7 +157,7 @@ func NewApiExposure(apiBasePath, zoneName string, appName string) *apiv1.ApiExpo M2M: &apiapi.Machine2MachineAuthentication{ ExternalIDP: &apiapi.ExternalIdentityProvider{ TokenEndpoint: "https://example.com/token", - TokenRequest: "client_secret_basic", + TokenRequest: apiapi.TokenRequestClientSecretBasic, GrantType: "client_credentials", Client: &apiapi.OAuth2ClientCredentials{ ClientId: "client-id", @@ -393,7 +393,7 @@ var _ = Describe("ApiExposure Controller", Ordered, func() { By("Creating the second APIExposure resource") thirdApiExposure.Spec.Security.M2M = &apiv1.Machine2MachineAuthentication{ ExternalIDP: &apiv1.ExternalIdentityProvider{ - TokenRequest: "sky", + TokenRequest: apiv1.TokenRequestMethod("sky"), }, Scopes: []string{"team:scope", "api:scope"}, } @@ -414,7 +414,7 @@ var _ = Describe("ApiExposure Controller", Ordered, func() { thirdApiExposure.Spec.Security.M2M = &apiv1.Machine2MachineAuthentication{ ExternalIDP: &apiv1.ExternalIdentityProvider{ TokenEndpoint: "https://example.com/token", - TokenRequest: "client_secret_basic", + TokenRequest: apiv1.TokenRequestClientSecretBasic, GrantType: "client_credentials", Client: &apiv1.OAuth2ClientCredentials{ ClientId: "team", @@ -440,7 +440,7 @@ var _ = Describe("ApiExposure Controller", Ordered, func() { g.Expect(route.Spec.Security.M2M.Scopes).To(Equal([]string{"team:scope", "api:scope"})) g.Expect(route.Spec.Security.M2M.ExternalIDP.TokenEndpoint).To(Equal("https://example.com/token")) - g.Expect(route.Spec.Security.M2M.ExternalIDP.TokenRequest).To(Equal("client_secret_basic")) + g.Expect(route.Spec.Security.M2M.ExternalIDP.TokenRequest).To(Equal(gatewayapi.TokenRequestClientSecretBasic)) g.Expect(route.Spec.Security.M2M.ExternalIDP.GrantType).To(Equal("client_credentials")) }, timeout, interval).Should(Succeed()) }) diff --git a/api/internal/controller/apisubscription_controller_ratelimiting_test.go b/api/internal/controller/apisubscription_controller_ratelimiting_test.go index d0d79d7e9..1cc331992 100644 --- a/api/internal/controller/apisubscription_controller_ratelimiting_test.go +++ b/api/internal/controller/apisubscription_controller_ratelimiting_test.go @@ -83,7 +83,7 @@ func NewApiExposureWithRateLimit(apiBasePath, zoneName, consumerClientId string, M2M: &apiapi.Machine2MachineAuthentication{ ExternalIDP: &apiapi.ExternalIdentityProvider{ TokenEndpoint: "https://example.com/token", - TokenRequest: "client_secret_basic", + TokenRequest: apiapi.TokenRequestClientSecretBasic, GrantType: "client_credentials", Client: &apiapi.OAuth2ClientCredentials{ ClientId: "client-id", diff --git a/api/internal/handler/util/route_util.go b/api/internal/handler/util/route_util.go index f4d85a347..ea6cc7499 100644 --- a/api/internal/handler/util/route_util.go +++ b/api/internal/handler/util/route_util.go @@ -520,7 +520,7 @@ func mapSecurity(apiSecurity *apiapi.Security) *gatewayapi.Security { if apiSecurity.M2M.ExternalIDP != nil { security.M2M.ExternalIDP = &gatewayapi.ExternalIdentityProvider{ TokenEndpoint: apiSecurity.M2M.ExternalIDP.TokenEndpoint, - TokenRequest: apiSecurity.M2M.ExternalIDP.TokenRequest, + TokenRequest: gatewayapi.TokenRequestMethod(apiSecurity.M2M.ExternalIDP.TokenRequest), GrantType: apiSecurity.M2M.ExternalIDP.GrantType, } if apiSecurity.M2M.ExternalIDP.Basic != nil { diff --git a/discovery-server/internal/mapper/apiexposure/out.go b/discovery-server/internal/mapper/apiexposure/out.go index 3c8af66e0..bac3cf23c 100644 --- a/discovery-server/internal/mapper/apiexposure/out.go +++ b/discovery-server/internal/mapper/apiexposure/out.go @@ -14,11 +14,11 @@ import ( ) // tokenRequestCRDToAPI converts CRD tokenRequest values to discovery-server API enum values. -func tokenRequestCRDToAPI(value string) api.OAuth2TokenRequest { - switch strings.ToLower(value) { - case "client_secret_basic": +func tokenRequestCRDToAPI(value apiv1.TokenRequestMethod) api.OAuth2TokenRequest { + switch value { + case apiv1.TokenRequestClientSecretBasic: return api.Header - case "client_secret_post": + case apiv1.TokenRequestClientSecretPost: return api.Body default: return api.OAuth2TokenRequest(value) diff --git a/discovery-server/internal/mapper/apiexposure/out_test.go b/discovery-server/internal/mapper/apiexposure/out_test.go index 587e57c1c..00e85084b 100644 --- a/discovery-server/internal/mapper/apiexposure/out_test.go +++ b/discovery-server/internal/mapper/apiexposure/out_test.go @@ -141,7 +141,7 @@ func TestMapSecurity(t *testing.T) { { name: "external idp oauth2", setup: func(in *apiv1.ApiExposure) { - in.Spec.Security = &apiv1.Security{M2M: &apiv1.Machine2MachineAuthentication{ExternalIDP: &apiv1.ExternalIdentityProvider{TokenEndpoint: "https://idp/token", TokenRequest: "client_secret_post", GrantType: "client_credentials", Client: &apiv1.OAuth2ClientCredentials{ClientId: "cid", ClientSecret: "sec"}}, Scopes: []string{"s1"}}} + in.Spec.Security = &apiv1.Security{M2M: &apiv1.Machine2MachineAuthentication{ExternalIDP: &apiv1.ExternalIdentityProvider{TokenEndpoint: "https://idp/token", TokenRequest: apiv1.TokenRequestClientSecretPost, GrantType: "client_credentials", Client: &apiv1.OAuth2ClientCredentials{ClientId: "cid", ClientSecret: "sec"}}, Scopes: []string{"s1"}}} }, assert: func(t *testing.T, out api.ApiExposureResponse) { t.Helper() @@ -157,7 +157,7 @@ func TestMapSecurity(t *testing.T) { { name: "external idp oauth2 with basic credentials", setup: func(in *apiv1.ApiExposure) { - in.Spec.Security = &apiv1.Security{M2M: &apiv1.Machine2MachineAuthentication{ExternalIDP: &apiv1.ExternalIdentityProvider{TokenEndpoint: "https://idp/token", TokenRequest: "client_secret_basic", GrantType: "password", Basic: &apiv1.BasicAuthCredentials{Username: "bu", Password: "bp"}}}} + in.Spec.Security = &apiv1.Security{M2M: &apiv1.Machine2MachineAuthentication{ExternalIDP: &apiv1.ExternalIdentityProvider{TokenEndpoint: "https://idp/token", TokenRequest: apiv1.TokenRequestClientSecretBasic, GrantType: "password", Basic: &apiv1.BasicAuthCredentials{Username: "bu", Password: "bp"}}}} }, assert: func(t *testing.T, out api.ApiExposureResponse) { t.Helper() diff --git a/gateway/api/v1/security_types.go b/gateway/api/v1/security_types.go index 467278792..f9800233a 100644 --- a/gateway/api/v1/security_types.go +++ b/gateway/api/v1/security_types.go @@ -4,6 +4,15 @@ package v1 +// TokenRequestMethod defines the token endpoint authentication method (RFC 7591). +// +kubebuilder:validation:Enum=client_secret_basic;client_secret_post +type TokenRequestMethod string + +const ( + TokenRequestClientSecretBasic TokenRequestMethod = "client_secret_basic" + TokenRequestClientSecretPost TokenRequestMethod = "client_secret_post" +) + type Security struct { // DisableAccessControl disable the ACL mechanism for this route // +kubebuilder:validation:Optional @@ -114,8 +123,7 @@ type ExternalIdentityProvider struct { // TokenRequest configures the token endpoint authentication method (RFC 7591) // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=client_secret_basic;client_secret_post - TokenRequest string `json:"tokenRequest,omitempty"` + TokenRequest TokenRequestMethod `json:"tokenRequest,omitempty"` // GrantType is the grant type for the external IDP authentication // +kubebuilder:validation:Optional // +kubebuilder:validation:Enum=client_credentials;authorization_code;password diff --git a/gateway/internal/controller/route_controller_test.go b/gateway/internal/controller/route_controller_test.go index aceb4fdb8..01d111410 100644 --- a/gateway/internal/controller/route_controller_test.go +++ b/gateway/internal/controller/route_controller_test.go @@ -144,7 +144,7 @@ var _ = Describe("Route Controller", Ordered, func() { }) It("should not accept a Route with TokenRequest=\"sky\"", func() { By("Creating the Route with TokenRequest=\"sky\"") - route.Spec.Security.M2M.ExternalIDP.TokenRequest = "sky" + route.Spec.Security.M2M.ExternalIDP.TokenRequest = gatewayv1.TokenRequestMethod("sky") err := k8sClient.Create(ctx, route) Expect(err).To(HaveOccurred()) Expect(apierrors.IsInvalid(err)).To(BeTrue()) diff --git a/gateway/internal/features/builder_external_idp_test.go b/gateway/internal/features/builder_external_idp_test.go index 5f1155452..3445bca88 100644 --- a/gateway/internal/features/builder_external_idp_test.go +++ b/gateway/internal/features/builder_external_idp_test.go @@ -215,7 +215,7 @@ func externalIDPProviderRouteOAuth() *gatewayv1.Route { M2M: &gatewayv1.Machine2MachineAuthentication{ ExternalIDP: &gatewayv1.ExternalIdentityProvider{ TokenEndpoint: "https://example.com/tokenEndpoint", - TokenRequest: "client_secret_basic", + TokenRequest: gatewayv1.TokenRequestClientSecretBasic, GrantType: "client_credentials", Client: &gatewayv1.OAuth2ClientCredentials{ ClientId: "gateway", @@ -243,7 +243,7 @@ func externalIDPProviderRouteBasic() *gatewayv1.Route { M2M: &gatewayv1.Machine2MachineAuthentication{ ExternalIDP: &gatewayv1.ExternalIdentityProvider{ TokenEndpoint: "https://example.com/tokenEndpoint", - TokenRequest: "client_secret_basic", + TokenRequest: gatewayv1.TokenRequestClientSecretBasic, GrantType: "password", Basic: &gatewayv1.BasicAuthCredentials{ Username: "user", @@ -271,7 +271,7 @@ func externalIDPProviderRouteOAuthJwt() *gatewayv1.Route { M2M: &gatewayv1.Machine2MachineAuthentication{ ExternalIDP: &gatewayv1.ExternalIdentityProvider{ TokenEndpoint: "https://example.com/tokenEndpoint", - TokenRequest: "client_secret_basic", + TokenRequest: gatewayv1.TokenRequestClientSecretBasic, GrantType: "client_credentials", Client: &gatewayv1.OAuth2ClientCredentials{ ClientId: "ClientId", diff --git a/gateway/internal/features/feature/external_idp.go b/gateway/internal/features/feature/external_idp.go index a1a58ad7c..a94b26260 100644 --- a/gateway/internal/features/feature/external_idp.go +++ b/gateway/internal/features/feature/external_idp.go @@ -189,11 +189,11 @@ func extendBasic(ctx context.Context, in plugin.OauthCredentials, providerSettin } // tokenRequestToJumper converts CRD tokenRequest values to the values expected by the Jumper plugin. -func tokenRequestToJumper(value string) (string, error) { - switch strings.ToLower(value) { - case "client_secret_basic": +func tokenRequestToJumper(value gatewayv1.TokenRequestMethod) (string, error) { + switch value { + case gatewayv1.TokenRequestClientSecretBasic: return "header", nil - case "client_secret_post": + case gatewayv1.TokenRequestClientSecretPost: return "body", nil default: return "", fmt.Errorf("unsupported tokenRequest value %q", value) diff --git a/rover-server/internal/mapper/rover/in/exposure.go b/rover-server/internal/mapper/rover/in/exposure.go index c6016af5b..7d21a044b 100644 --- a/rover-server/internal/mapper/rover/in/exposure.go +++ b/rover-server/internal/mapper/rover/in/exposure.go @@ -15,21 +15,20 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "github.com/telekom/controlplane/rover-server/internal/api" - rovermapper "github.com/telekom/controlplane/rover-server/internal/mapper/rover" ) // oauth2TokenRequestToCRD maps API tokenRequest values to CRD tokenRequest values. -var oauth2TokenRequestToCRD = map[string]string{ - "body": rovermapper.TokenRequestClientSecretPost, - "header": rovermapper.TokenRequestClientSecretBasic, - "basic": rovermapper.TokenRequestClientSecretBasic, +var oauth2TokenRequestToCRD = map[string]roverv1.TokenRequestMethod{ + "body": roverv1.TokenRequestClientSecretPost, + "header": roverv1.TokenRequestClientSecretBasic, + "basic": roverv1.TokenRequestClientSecretBasic, } -func tokenRequestAPIToCRD(value string) string { +func tokenRequestAPIToCRD(value string) roverv1.TokenRequestMethod { if mapped, ok := oauth2TokenRequestToCRD[strings.ToLower(value)]; ok { return mapped } - return value + return roverv1.TokenRequestMethod(value) } func mapExposure(in *api.Exposure, out *roverv1.Exposure) error { diff --git a/rover-server/internal/mapper/rover/in/exposure_security_test.go b/rover-server/internal/mapper/rover/in/exposure_security_test.go index abf25755f..7d44499f8 100644 --- a/rover-server/internal/mapper/rover/in/exposure_security_test.go +++ b/rover-server/internal/mapper/rover/in/exposure_security_test.go @@ -69,7 +69,7 @@ var _ = Describe("Exposure Security Mapper", func() { Expect(output.Security.M2M).ToNot(BeNil()) Expect(output.Security.M2M.ExternalIDP).ToNot(BeNil()) Expect(output.Security.M2M.ExternalIDP.TokenEndpoint).To(Equal("https://test.com/token")) - Expect(output.Security.M2M.ExternalIDP.TokenRequest).To(Equal("client_secret_basic")) + Expect(output.Security.M2M.ExternalIDP.TokenRequest).To(Equal(roverv1.TokenRequestClientSecretBasic)) Expect(output.Security.M2M.ExternalIDP.GrantType).To(Equal("client_credentials")) Expect(output.Security.M2M.ExternalIDP.Client).ToNot(BeNil()) Expect(output.Security.M2M.ExternalIDP.Client.ClientId).To(Equal("client-id")) diff --git a/rover-server/internal/mapper/rover/in/rover.go b/rover-server/internal/mapper/rover/in/rover.go index 0649123b3..f7b705693 100644 --- a/rover-server/internal/mapper/rover/in/rover.go +++ b/rover-server/internal/mapper/rover/in/rover.go @@ -13,7 +13,6 @@ import ( "github.com/telekom/controlplane/rover-server/internal/api" "github.com/telekom/controlplane/rover-server/internal/mapper" - rovermapper "github.com/telekom/controlplane/rover-server/internal/mapper/rover" ) func MapRequest(in *api.RoverUpdateRequest, id mapper.ResourceIdInfo) (res *roverv1.Rover, err error) { @@ -153,9 +152,9 @@ func mapPermissions(in *api.Rover, out *roverv1.Rover) error { } // clientAuthMethodToCRD maps rover-server API enum values to rover CRD tokenRequest values. -var clientAuthMethodToCRD = map[api.AuthenticationClientAuthMethod]string{ - api.BASIC: rovermapper.TokenRequestClientSecretBasic, - api.POST: rovermapper.TokenRequestClientSecretPost, +var clientAuthMethodToCRD = map[api.AuthenticationClientAuthMethod]roverv1.TokenRequestMethod{ + api.BASIC: roverv1.TokenRequestClientSecretBasic, + api.POST: roverv1.TokenRequestClientSecretPost, } func mapAuthentication(in *api.Rover, out *roverv1.Rover) { diff --git a/rover-server/internal/mapper/rover/in/rover_test.go b/rover-server/internal/mapper/rover/in/rover_test.go index fbf9dadaf..cc8c3c963 100644 --- a/rover-server/internal/mapper/rover/in/rover_test.go +++ b/rover-server/internal/mapper/rover/in/rover_test.go @@ -155,7 +155,7 @@ var _ = Describe("Rover Mapper", func() { Expect(err).ToNot(HaveOccurred()) Expect(output.Spec.Authentication).ToNot(BeNil()) Expect(output.Spec.Authentication.M2M).ToNot(BeNil()) - Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("client_secret_basic")) + Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal(roverv1.TokenRequestClientSecretBasic)) }) It("must map POST to client_secret_post in CRD", func() { @@ -172,7 +172,7 @@ var _ = Describe("Rover Mapper", func() { Expect(err).ToNot(HaveOccurred()) Expect(output.Spec.Authentication).ToNot(BeNil()) Expect(output.Spec.Authentication.M2M).ToNot(BeNil()) - Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("client_secret_post")) + Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal(roverv1.TokenRequestClientSecretPost)) }) It("must not set authentication when clientAuthMethod is empty", func() { @@ -201,7 +201,7 @@ var _ = Describe("Rover Mapper", func() { Expect(err).ToNot(HaveOccurred()) Expect(output.Spec.Authentication).ToNot(BeNil()) Expect(output.Spec.Authentication.M2M).ToNot(BeNil()) - Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("client_secret_basic")) + Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal(roverv1.TokenRequestClientSecretBasic)) }) It("must fuzzy-match 'body' to client_secret_post in CRD", func() { @@ -218,7 +218,7 @@ var _ = Describe("Rover Mapper", func() { Expect(err).ToNot(HaveOccurred()) Expect(output.Spec.Authentication).ToNot(BeNil()) Expect(output.Spec.Authentication.M2M).ToNot(BeNil()) - Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal("client_secret_post")) + Expect(output.Spec.Authentication.M2M.TokenRequest).To(Equal(roverv1.TokenRequestClientSecretPost)) }) }) diff --git a/rover-server/internal/mapper/rover/out/exposure.go b/rover-server/internal/mapper/rover/out/exposure.go index eaebdf4e9..1b26ba0bb 100644 --- a/rover-server/internal/mapper/rover/out/exposure.go +++ b/rover-server/internal/mapper/rover/out/exposure.go @@ -12,17 +12,16 @@ import ( roverv1 "github.com/telekom/controlplane/rover/api/v1" "github.com/telekom/controlplane/rover-server/internal/api" - rovermapper "github.com/telekom/controlplane/rover-server/internal/mapper/rover" ) // oauth2TokenRequestCRDToAPI maps CRD tokenRequest values to API Oauth2TokenRequest values. -var oauth2TokenRequestCRDToAPI = map[string]api.Oauth2TokenRequest{ - rovermapper.TokenRequestClientSecretBasic: api.Header, - rovermapper.TokenRequestClientSecretPost: api.Body, +var oauth2TokenRequestCRDToAPI = map[roverv1.TokenRequestMethod]api.Oauth2TokenRequest{ + roverv1.TokenRequestClientSecretBasic: api.Header, + roverv1.TokenRequestClientSecretPost: api.Body, } -func tokenRequestCRDToAPI(value string) api.Oauth2TokenRequest { - if mapped, ok := oauth2TokenRequestCRDToAPI[strings.ToLower(value)]; ok { +func tokenRequestCRDToAPI(value roverv1.TokenRequestMethod) api.Oauth2TokenRequest { + if mapped, ok := oauth2TokenRequestCRDToAPI[value]; ok { return mapped } return api.Oauth2TokenRequest(value) diff --git a/rover-server/internal/mapper/rover/out/exposure_security_test.go b/rover-server/internal/mapper/rover/out/exposure_security_test.go index f6c420131..882cfcb66 100644 --- a/rover-server/internal/mapper/rover/out/exposure_security_test.go +++ b/rover-server/internal/mapper/rover/out/exposure_security_test.go @@ -51,7 +51,7 @@ var _ = Describe("Exposure Security Mapper (Out)", func() { M2M: &roverv1.Machine2MachineAuthentication{ ExternalIDP: &roverv1.ExternalIdentityProvider{ TokenEndpoint: "https://test.com/token", - TokenRequest: "client_secret_basic", + TokenRequest: roverv1.TokenRequestClientSecretBasic, GrantType: "client_credentials", Client: &roverv1.OAuth2ClientCredentials{ ClientId: "client-id", diff --git a/rover-server/internal/mapper/rover/out/rover.go b/rover-server/internal/mapper/rover/out/rover.go index 86d7b8424..bac1690fd 100644 --- a/rover-server/internal/mapper/rover/out/rover.go +++ b/rover-server/internal/mapper/rover/out/rover.go @@ -12,7 +12,6 @@ import ( "github.com/telekom/controlplane/rover-server/internal/api" "github.com/telekom/controlplane/rover-server/internal/mapper" - rovermapper "github.com/telekom/controlplane/rover-server/internal/mapper/rover" "github.com/telekom/controlplane/rover-server/internal/mapper/status" "github.com/telekom/controlplane/rover-server/pkg/store" ) @@ -52,9 +51,9 @@ func MapRover(in *roverv1.Rover, out *api.Rover) error { } // tokenRequestToAPI maps rover CRD tokenRequest values to rover-server API enum values. -var tokenRequestToAPI = map[string]api.AuthenticationClientAuthMethod{ - rovermapper.TokenRequestClientSecretBasic: api.BASIC, - rovermapper.TokenRequestClientSecretPost: api.POST, +var tokenRequestToAPI = map[roverv1.TokenRequestMethod]api.AuthenticationClientAuthMethod{ + roverv1.TokenRequestClientSecretBasic: api.BASIC, + roverv1.TokenRequestClientSecretPost: api.POST, } func mapAuthentication(in *roverv1.Rover, out *api.Rover) { diff --git a/rover-server/internal/mapper/rover/out/rover_test.go b/rover-server/internal/mapper/rover/out/rover_test.go index 8916b083a..9664e890b 100644 --- a/rover-server/internal/mapper/rover/out/rover_test.go +++ b/rover-server/internal/mapper/rover/out/rover_test.go @@ -64,7 +64,7 @@ var _ = Describe("Rover Mapper", func() { input := rover.DeepCopy() input.Spec.Authentication = &roverv1.RoverAuthentication{ M2M: &roverv1.RoverM2MAuthentication{ - TokenRequest: "client_secret_basic", + TokenRequest: roverv1.TokenRequestClientSecretBasic, }, } output := &api.Rover{} @@ -79,7 +79,7 @@ var _ = Describe("Rover Mapper", func() { input := rover.DeepCopy() input.Spec.Authentication = &roverv1.RoverAuthentication{ M2M: &roverv1.RoverM2MAuthentication{ - TokenRequest: "client_secret_post", + TokenRequest: roverv1.TokenRequestClientSecretPost, }, } output := &api.Rover{} diff --git a/rover-server/internal/mapper/rover/token_request.go b/rover-server/internal/mapper/rover/token_request.go deleted file mode 100644 index 55baba1a6..000000000 --- a/rover-server/internal/mapper/rover/token_request.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2026 Deutsche Telekom IT GmbH -// -// SPDX-License-Identifier: Apache-2.0 - -package rover - -// CRD tokenRequest values (RFC 7591 client authentication methods). -const ( - TokenRequestClientSecretBasic = "client_secret_basic" - TokenRequestClientSecretPost = "client_secret_post" -) diff --git a/rover/api/v1/rover_types.go b/rover/api/v1/rover_types.go index 20a127294..0756d78d9 100644 --- a/rover/api/v1/rover_types.go +++ b/rover/api/v1/rover_types.go @@ -219,9 +219,8 @@ type RoverM2MAuthentication struct { // This feature is currently only documented but not parsed towards the application and identity domain as it is still in discussion whether // this should will be enforced for IDPs. // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=client_secret_basic;client_secret_post // +kubebuilder:default=client_secret_basic - TokenRequest string `json:"tokenRequest,omitempty"` + TokenRequest TokenRequestMethod `json:"tokenRequest,omitempty"` } // Exposure defines a service that is exposed by this Rover diff --git a/rover/api/v1/security_types.go b/rover/api/v1/security_types.go index 35d18474a..abcc94439 100644 --- a/rover/api/v1/security_types.go +++ b/rover/api/v1/security_types.go @@ -4,6 +4,15 @@ package v1 +// TokenRequestMethod defines the token endpoint authentication method (RFC 7591). +// +kubebuilder:validation:Enum=client_secret_basic;client_secret_post +type TokenRequestMethod string + +const ( + TokenRequestClientSecretBasic TokenRequestMethod = "client_secret_basic" + TokenRequestClientSecretPost TokenRequestMethod = "client_secret_post" +) + // Security defines the security configuration for the Rover // Security is optional, but if provided, exactly one of m2m or h2m must be set type Security struct { @@ -65,8 +74,7 @@ type ExternalIdentityProvider struct { // TokenRequest configures the token endpoint authentication method (RFC 7591) // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=client_secret_basic;client_secret_post - TokenRequest string `json:"tokenRequest,omitempty"` + TokenRequest TokenRequestMethod `json:"tokenRequest,omitempty"` // GrantType defines the OAuth2 grant type to use for the token request // +kubebuilder:validation:Optional diff --git a/rover/internal/controller/rover_controller_test.go b/rover/internal/controller/rover_controller_test.go index 8d0927607..75a6b495e 100644 --- a/rover/internal/controller/rover_controller_test.go +++ b/rover/internal/controller/rover_controller_test.go @@ -472,7 +472,7 @@ var _ = Describe("Rover Controller", Ordered, func() { M2M: &roverv1.Machine2MachineAuthentication{ ExternalIDP: &roverv1.ExternalIdentityProvider{ TokenEndpoint: "https://idp.example.com/token", - TokenRequest: "client_secret_basic", + TokenRequest: roverv1.TokenRequestClientSecretBasic, GrantType: "client_credentials", Basic: nil, Client: &roverv1.OAuth2ClientCredentials{ @@ -524,7 +524,7 @@ var _ = Describe("Rover Controller", Ordered, func() { g.Expect(apiExposure.Spec.Security.M2M.ExternalIDP.Client.ClientId).To(Equal("clientID")) g.Expect(apiExposure.Spec.Security.M2M.ExternalIDP.Client.ClientSecret).To(Equal("******")) g.Expect(apiExposure.Spec.Security.M2M.Scopes[0]).To(Equal("eIDP:scope")) - g.Expect(apiExposure.Spec.Security.M2M.ExternalIDP.TokenRequest).To(Equal("client_secret_basic")) + g.Expect(apiExposure.Spec.Security.M2M.ExternalIDP.TokenRequest).To(Equal(apiapi.TokenRequestClientSecretBasic)) g.Expect(apiExposure.Spec.Security.M2M.ExternalIDP.TokenEndpoint).To(Equal("https://idp.example.com/token")) g.Expect(apiExposure.Spec.Security.M2M.ExternalIDP.GrantType).To(Equal("client_credentials")) }, timeout, interval).Should(Succeed()) diff --git a/rover/internal/handler/rover/api/exposure.go b/rover/internal/handler/rover/api/exposure.go index ed5832ca1..6ffa9efd2 100644 --- a/rover/internal/handler/rover/api/exposure.go +++ b/rover/internal/handler/rover/api/exposure.go @@ -142,7 +142,7 @@ func mapSecurityToApiSecurity(roverSecurity *rover.Security) *apiapi.Security { if roverSecurity.M2M.ExternalIDP != nil { security.M2M.ExternalIDP = &apiapi.ExternalIdentityProvider{ TokenEndpoint: roverSecurity.M2M.ExternalIDP.TokenEndpoint, - TokenRequest: roverSecurity.M2M.ExternalIDP.TokenRequest, + TokenRequest: apiapi.TokenRequestMethod(roverSecurity.M2M.ExternalIDP.TokenRequest), GrantType: roverSecurity.M2M.ExternalIDP.GrantType, Client: toApiClient(roverSecurity.M2M.ExternalIDP.Client), Basic: toApiBasic(roverSecurity.M2M.ExternalIDP.Basic), From 68dad009d53067097b5641f579428674d25734c2 Mon Sep 17 00:00:00 2001 From: stefan-ctrl Date: Mon, 18 May 2026 07:48:43 +0200 Subject: [PATCH 16/16] refactor: update PatchAuthentication to remove normalization of clientAuthMethod --- rover-ctl/pkg/handlers/v0/rover.go | 27 ++----------------------- rover-ctl/pkg/handlers/v0/rover_test.go | 12 +++++------ 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/rover-ctl/pkg/handlers/v0/rover.go b/rover-ctl/pkg/handlers/v0/rover.go index 19ca7cb62..80352c478 100644 --- a/rover-ctl/pkg/handlers/v0/rover.go +++ b/rover-ctl/pkg/handlers/v0/rover.go @@ -7,7 +7,6 @@ package v0 import ( "context" "maps" - "strings" "github.com/pkg/errors" "github.com/telekom/controlplane/rover-ctl/pkg/handlers/common" @@ -74,8 +73,7 @@ func PatchRoverRequest(ctx context.Context, obj types.Object) error { // PatchAuthentication restructures spec.authentication.m2m.clientAuthMethod // into spec.authentication.clientAuthMethod for the rover-server API format. -// It also normalizes "BODY"/"body" to "POST" since the server schema only accepts -// NONE, POST, BASIC. +// The server performs fuzzy matching on the value, so no normalization is needed here. func PatchAuthentication(spec map[string]any) { auth, exists := spec["authentication"] if !exists { @@ -101,28 +99,7 @@ func PatchAuthentication(spec map[string]any) { } spec["authentication"] = map[string]any{ - "clientAuthMethod": normalizeClientAuthMethod(clientAuthMethod), - } -} - -// normalizeClientAuthMethod maps user-friendly aliases to the API enum values. -// "BODY"/"body" is treated as "POST" per RFC 6749. -func normalizeClientAuthMethod(value any) any { - s, ok := value.(string) - if !ok { - return value - } - switch strings.ToUpper(s) { - case "BODY": - return "POST" - case "BASIC": - return "BASIC" - case "NONE": - return "NONE" - case "POST": - return "POST" - default: - return value + "clientAuthMethod": clientAuthMethod, } } diff --git a/rover-ctl/pkg/handlers/v0/rover_test.go b/rover-ctl/pkg/handlers/v0/rover_test.go index 047c32b5a..dc77d3aea 100644 --- a/rover-ctl/pkg/handlers/v0/rover_test.go +++ b/rover-ctl/pkg/handlers/v0/rover_test.go @@ -382,7 +382,7 @@ var _ = Describe("Rover Handler", func() { }) Describe("PatchAuthentication", func() { - It("should normalize 'basic' to 'BASIC' in authentication.clientAuthMethod", func() { + It("should pass through 'basic' as-is in authentication.clientAuthMethod", func() { obj := &types.UnstructuredObject{ Content: map[string]any{ "spec": map[string]any{ @@ -401,10 +401,10 @@ var _ = Describe("Rover Handler", func() { content := obj.GetContent() auth, ok := content["authentication"].(map[string]any) Expect(ok).To(BeTrue()) - Expect(auth["clientAuthMethod"]).To(Equal("BASIC")) + Expect(auth["clientAuthMethod"]).To(Equal("basic")) }) - It("should normalize 'body' to 'POST' in authentication.clientAuthMethod", func() { + It("should pass through 'body' as-is in authentication.clientAuthMethod", func() { obj := &types.UnstructuredObject{ Content: map[string]any{ "spec": map[string]any{ @@ -423,10 +423,10 @@ var _ = Describe("Rover Handler", func() { content := obj.GetContent() auth, ok := content["authentication"].(map[string]any) Expect(ok).To(BeTrue()) - Expect(auth["clientAuthMethod"]).To(Equal("POST")) + Expect(auth["clientAuthMethod"]).To(Equal("body")) }) - It("should normalize 'BODY' to 'POST' in authentication.clientAuthMethod", func() { + It("should pass through 'BODY' as-is in authentication.clientAuthMethod", func() { obj := &types.UnstructuredObject{ Content: map[string]any{ "spec": map[string]any{ @@ -445,7 +445,7 @@ var _ = Describe("Rover Handler", func() { content := obj.GetContent() auth, ok := content["authentication"].(map[string]any) Expect(ok).To(BeTrue()) - Expect(auth["clientAuthMethod"]).To(Equal("POST")) + Expect(auth["clientAuthMethod"]).To(Equal("BODY")) }) It("should not add authentication when it is missing", func() {