Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
86552d6
feat(rover-ctl): implement PatchAuthentication to map clientAuthMetho…
stefan-ctrl Apr 27, 2026
90e88b3
Merge branch 'main' into client-authentication-method-roverctl-only
stefan-ctrl Apr 27, 2026
82994b5
test(rover-server): add tests to verify ctl output
stefan-ctrl Apr 27, 2026
de29049
Merge branch 'client-authentication-method-roverctl-only' of https://…
stefan-ctrl Apr 27, 2026
55eeea7
test(rover-handler): update tests to ensure authentication remains un…
stefan-ctrl Apr 27, 2026
dad4b83
fix(timestamps): update snapshot timestamps to reflect correct timezo…
stefan-ctrl Apr 27, 2026
08f3b3c
Merge branch 'main' into client-authentication-method-roverctl-only
stefan-ctrl Apr 27, 2026
9bc4511
feat(rover): implement mapping for Rover authentication methods and a…
stefan-ctrl Apr 27, 2026
d9fafe0
chore: make generate & manifest
stefan-ctrl Apr 27, 2026
63a4d66
refactor: align with tokenRequest values
stefan-ctrl Apr 28, 2026
848caa5
Merge branch 'main' into client-authentication-method-roverctl-only
stefan-ctrl Apr 28, 2026
bd3e69a
Merge branch 'main' into client-authentication-method-roverctl-only
stefan-ctrl Apr 28, 2026
59b6a0a
chore: make generate & manifests
stefan-ctrl Apr 28, 2026
8de6c66
refactor: move to fuzzbuzz converter
stefan-ctrl Apr 28, 2026
10ae067
refactor: align tokenRequests values for RFC 7591
stefan-ctrl Apr 28, 2026
fe7df33
refactor: replace hardcoded tokenRequest values with constants from r…
stefan-ctrl Apr 28, 2026
4e08c17
Merge branch 'main' into client-authentication-method-roverctl-only
stefan-ctrl Apr 28, 2026
5e931dd
feat(tokenRequest): add initial tokenRequest values for RFC 7591 clie…
stefan-ctrl Apr 28, 2026
e56556a
Merge remote-tracking branch 'origin/main' into client-authentication…
stefan-ctrl May 5, 2026
4484ab4
feat(auth): normalize clientAuthMethod values in PatchAuthentication …
stefan-ctrl May 5, 2026
ec4f672
Merge remote-tracking branch 'origin/main' into client-authentication…
stefan-ctrl May 13, 2026
99d318d
chore: solve some local issues
stefan-ctrl May 13, 2026
b2bd0ff
Merge branch 'main' into client-authentication-method-roverctl-only
stefan-ctrl May 13, 2026
9627521
Merge branch 'main' into client-authentication-method-roverctl-only
stefan-ctrl May 13, 2026
5dcb703
refactor: introduce enum
stefan-ctrl May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions api/api/v1/security_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -64,10 +73,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
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
Expand Down
8 changes: 4 additions & 4 deletions api/config/crd/bases/api.cp.ei.telekom.de_apiexposures.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions api/internal/controller/apiexposure_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

import (
"fmt"

"github.com/telekom/controlplane/api/internal/handler/util"

Check failure on line 10 in api/internal/controller/apiexposure_controller_test.go

View workflow job for this annotation

GitHub Actions / Api / Static Checks for api

File is not properly formatted (gci)
applicationapi "github.com/telekom/controlplane/application/api/v1"

adminapi "github.com/telekom/controlplane/admin/api/v1"
Expand Down Expand Up @@ -156,7 +157,7 @@
M2M: &apiapi.Machine2MachineAuthentication{
ExternalIDP: &apiapi.ExternalIdentityProvider{
TokenEndpoint: "https://example.com/token",
TokenRequest: "header",
TokenRequest: apiapi.TokenRequestClientSecretBasic,
GrantType: "client_credentials",
Client: &apiapi.OAuth2ClientCredentials{
ClientId: "client-id",
Expand Down Expand Up @@ -392,14 +393,14 @@
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"},
}
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)
Expand All @@ -413,7 +414,7 @@
thirdApiExposure.Spec.Security.M2M = &apiv1.Machine2MachineAuthentication{
ExternalIDP: &apiv1.ExternalIdentityProvider{
TokenEndpoint: "https://example.com/token",
TokenRequest: "header",
TokenRequest: apiv1.TokenRequestClientSecretBasic,
GrantType: "client_credentials",
Client: &apiv1.OAuth2ClientCredentials{
ClientId: "team",
Expand All @@ -439,7 +440,7 @@
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(gatewayapi.TokenRequestClientSecretBasic))
g.Expect(route.Spec.Security.M2M.ExternalIDP.GrantType).To(Equal("client_credentials"))
}, timeout, interval).Should(Succeed())
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
package controller

import (
. "github.com/onsi/ginkgo/v2"

Check failure on line 8 in api/internal/controller/apisubscription_controller_ratelimiting_test.go

View workflow job for this annotation

GitHub Actions / Api / Static Checks for api

File is not properly formatted (gci)
. "github.com/onsi/gomega"
adminapi "github.com/telekom/controlplane/admin/api/v1"
apiapi "github.com/telekom/controlplane/api/api/v1"
Expand Down Expand Up @@ -83,7 +83,7 @@
M2M: &apiapi.Machine2MachineAuthentication{
ExternalIDP: &apiapi.ExternalIdentityProvider{
TokenEndpoint: "https://example.com/token",
TokenRequest: "header",
TokenRequest: apiapi.TokenRequestClientSecretBasic,
GrantType: "client_credentials",
Client: &apiapi.OAuth2ClientCredentials{
ClientId: "client-id",
Expand Down
2 changes: 1 addition & 1 deletion api/internal/handler/util/route_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 13 additions & 1 deletion discovery-server/internal/mapper/apiexposure/out.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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 apiv1.TokenRequestMethod) api.OAuth2TokenRequest {
switch value {
case apiv1.TokenRequestClientSecretBasic:
return api.Header
case apiv1.TokenRequestClientSecretPost:
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{
Expand Down Expand Up @@ -81,7 +93,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,
}

Expand Down
4 changes: 2 additions & 2 deletions discovery-server/internal/mapper/apiexposure/out_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: 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()
Expand All @@ -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: apiv1.TokenRequestClientSecretBasic, GrantType: "password", Basic: &apiv1.BasicAuthCredentials{Username: "bu", Password: "bp"}}}}
},
assert: func(t *testing.T, out api.ApiExposureResponse) {
t.Helper()
Expand Down
14 changes: 11 additions & 3 deletions gateway/api/v1/security_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -112,10 +121,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
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
Expand Down
16 changes: 8 additions & 8 deletions gateway/config/crd/bases/gateway.cp.ei.telekom.de_routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions gateway/internal/controller/route_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
package controller

import (
. "github.com/onsi/ginkgo/v2"

Check failure on line 8 in gateway/internal/controller/route_controller_test.go

View workflow job for this annotation

GitHub Actions / Gateway / Static Checks for gateway

File is not properly formatted (gci)
. "github.com/onsi/gomega"
"github.com/telekom/controlplane/common/pkg/condition"
"github.com/telekom/controlplane/common/pkg/config"
Expand Down Expand Up @@ -144,11 +144,11 @@
})
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())
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() {
Expand Down
6 changes: 3 additions & 3 deletions gateway/internal/features/builder_external_idp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func externalIDPProviderRouteOAuth() *gatewayv1.Route {
M2M: &gatewayv1.Machine2MachineAuthentication{
ExternalIDP: &gatewayv1.ExternalIdentityProvider{
TokenEndpoint: "https://example.com/tokenEndpoint",
TokenRequest: "header",
TokenRequest: gatewayv1.TokenRequestClientSecretBasic,
GrantType: "client_credentials",
Client: &gatewayv1.OAuth2ClientCredentials{
ClientId: "gateway",
Expand Down Expand Up @@ -243,7 +243,7 @@ func externalIDPProviderRouteBasic() *gatewayv1.Route {
M2M: &gatewayv1.Machine2MachineAuthentication{
ExternalIDP: &gatewayv1.ExternalIdentityProvider{
TokenEndpoint: "https://example.com/tokenEndpoint",
TokenRequest: "header",
TokenRequest: gatewayv1.TokenRequestClientSecretBasic,
GrantType: "password",
Basic: &gatewayv1.BasicAuthCredentials{
Username: "user",
Expand Down Expand Up @@ -271,7 +271,7 @@ func externalIDPProviderRouteOAuthJwt() *gatewayv1.Route {
M2M: &gatewayv1.Machine2MachineAuthentication{
ExternalIDP: &gatewayv1.ExternalIdentityProvider{
TokenEndpoint: "https://example.com/tokenEndpoint",
TokenRequest: "header",
TokenRequest: gatewayv1.TokenRequestClientSecretBasic,
GrantType: "client_credentials",
Client: &gatewayv1.OAuth2ClientCredentials{
ClientId: "ClientId",
Expand Down
19 changes: 18 additions & 1 deletion gateway/internal/features/feature/external_idp.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package feature

import (
"context"
"fmt"
"strings"

"github.com/pkg/errors"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 gatewayv1.TokenRequestMethod) (string, error) {
switch value {
case gatewayv1.TokenRequestClientSecretBasic:
return "header", nil
case gatewayv1.TokenRequestClientSecretPost:
return "body", nil
default:
return "", fmt.Errorf("unsupported tokenRequest value %q", value)
}
}
57 changes: 57 additions & 0 deletions rover-ctl/pkg/handlers/v0/rover.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package v0
import (
"context"
"maps"
"strings"

"github.com/pkg/errors"
"github.com/telekom/controlplane/rover-ctl/pkg/handlers/common"
Expand Down Expand Up @@ -65,10 +66,66 @@ func PatchRoverRequest(ctx context.Context, obj types.Object) error {
}
}

PatchAuthentication(spec)

obj.SetContent(spec)
return nil
}

// 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.
func PatchAuthentication(spec map[string]any) {
auth, exists := spec["authentication"]
if !exists {
return
}
authMap, ok := auth.(map[string]any)
if !ok {
return
}

m2m, exists := authMap["m2m"]
if !exists {
return
}
m2mMap, ok := m2m.(map[string]any)
if !ok {
return
}

clientAuthMethod, exists := m2mMap["clientAuthMethod"]
if !exists {
return
}

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
}
}

func PatchExposures(exposures []any) []map[string]any {
if len(exposures) == 0 {
return nil
Expand Down
Loading
Loading