Allow scope to be passed as array (#150)

* feat: Allow scope to be passed as array

Scopes are currently passed as a scope string, separating scopes by
spaces.
Clients can grow to many scopes, resulting in a very long string.

This change allows us to specify scopes using the property scopeArray.
That way, we can separate scopes by newlines.
Additionally, this allows us to comment a single scope temporarily or
add a comment for a specific scope, e.g. as a reason why that client has
this scope granted.

* feat: Deprecate scope in favor of scopeArray

* feat: Use kubebuilder:deprecatedversion
This commit is contained in:
Tim Siebels 2024-10-14 10:06:41 +02:00 committed by GitHub
parent aa0bff206a
commit 44cd2371d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 79 additions and 6 deletions

View File

@ -145,12 +145,18 @@ type OAuth2ClientSpec struct {
// Audience is a whitelist defining the audiences this client is allowed to request tokens for
Audience []string `json:"audience,omitempty"`
// +kubebuilder:validation:Pattern=([a-zA-Z0-9\.\*]+\s?)+
// +kubebuilder:validation:Pattern=([a-zA-Z0-9\.\*]+\s?)*
// +kubebuilder:deprecatedversion:warning="Property scope is deprecated. Use scopeArray instead."
//
// Scope is a string containing a space-separated list of scope values (as
// described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client
// can use when requesting access tokens.
Scope string `json:"scope"`
// Use scopeArray instead.
Scope string `json:"scope,omitempty"`
// Scope is an array of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749])
// that the client can use when requesting access tokens.
ScopeArray []string `json:"scopeArray,omitempty"`
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253

View File

@ -92,7 +92,6 @@ func TestCreateAPI(t *testing.T) {
"invalid grant type": func() { created.Spec.GrantTypes = []GrantType{"invalid"} },
"invalid response type": func() { created.Spec.ResponseTypes = []ResponseType{"invalid", "code"} },
"invalid composite response type": func() { created.Spec.ResponseTypes = []ResponseType{"invalid code", "code id_token"} },
"invalid scope": func() { created.Spec.Scope = "" },
"missing secret name": func() { created.Spec.SecretName = "" },
"invalid redirect URI": func() { created.Spec.RedirectURIs = []RedirectURI{"invalid"} },
"invalid logout redirect URI": func() { created.Spec.PostLogoutRedirectURIs = []RedirectURI{"invalid"} },

View File

@ -147,6 +147,11 @@ func (in *OAuth2ClientSpec) DeepCopyInto(out *OAuth2ClientSpec) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ScopeArray != nil {
in, out := &in.ScopeArray, &out.ScopeArray
*out = make([]string, len(*in))
copy(*out, *in)
}
out.HydraAdmin = in.HydraAdmin
out.TokenLifespans = in.TokenLifespans
in.Metadata.DeepCopyInto(&out.Metadata)

View File

@ -202,8 +202,16 @@ spec:
Scope is a string containing a space-separated list of scope values (as
described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client
can use when requesting access tokens.
pattern: ([a-zA-Z0-9\.\*]+\s?)+
Use scopeArray instead.
pattern: ([a-zA-Z0-9\.\*]+\s?)*
type: string
scopeArray:
description: |-
Scope is an array of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749])
that the client can use when requesting access tokens.
items:
type: string
type: array
secretName:
description:
SecretName points to the K8s secret that contains this
@ -301,7 +309,6 @@ spec:
type: object
required:
- grantTypes
- scope
- secretName
type: object
status:

View File

@ -90,6 +90,7 @@ func StartTestManager(mgr manager.Manager) context.Context {
ctx := context.Background()
go func() {
defer GinkgoRecover()
defer ctx.Done()
Expect(mgr.Start(ctx)).NotTo(HaveOccurred())
}()

View File

@ -6,6 +6,7 @@ package hydra
import (
"encoding/json"
"fmt"
"strings"
"k8s.io/utils/ptr"
@ -67,6 +68,15 @@ func FromOAuth2Client(c *hydrav1alpha1.OAuth2Client) (*OAuth2ClientJSON, error)
return nil, fmt.Errorf("unable to encode `metadata` property value to json: %w", err)
}
if c.Spec.Scope != "" {
fmt.Println("Property `scope` in client '" + c.Name + "' is deprecated. Rather use scopeArray.")
}
var scope = c.Spec.Scope
if c.Spec.ScopeArray != nil {
scope = strings.Trim(strings.Join(c.Spec.ScopeArray, " ")+" "+scope, " ")
}
return &OAuth2ClientJSON{
ClientName: c.Spec.ClientName,
GrantTypes: grantToStringSlice(c.Spec.GrantTypes),
@ -75,7 +85,7 @@ func FromOAuth2Client(c *hydrav1alpha1.OAuth2Client) (*OAuth2ClientJSON, error)
PostLogoutRedirectURIs: redirectToStringSlice(c.Spec.PostLogoutRedirectURIs),
AllowedCorsOrigins: redirectToStringSlice(c.Spec.AllowedCorsOrigins),
Audience: c.Spec.Audience,
Scope: c.Spec.Scope,
Scope: scope,
SkipConsent: c.Spec.SkipConsent,
Owner: fmt.Sprintf("%s/%s", c.Name, c.Namespace),
TokenEndpointAuthMethod: string(c.Spec.TokenEndpointAuthMethod),

45
hydra/types_test.go Normal file
View File

@ -0,0 +1,45 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hydra_test
import (
"testing"
hydrav1alpha1 "github.com/ory/hydra-maester/api/v1alpha1"
"github.com/ory/hydra-maester/hydra"
"github.com/stretchr/testify/assert"
)
func TestTypes(t *testing.T) {
t.Run("Test ScopeArray", func(t *testing.T) {
c := hydrav1alpha1.OAuth2Client{
Spec: hydrav1alpha1.OAuth2ClientSpec{
ScopeArray: []string{"scope1", "scope2"},
},
}
var parsedClient, err = hydra.FromOAuth2Client(&c)
if err != nil {
assert.Fail(t, "unexpected error: %s", err)
}
assert.Equal(t, parsedClient.Scope, "scope1 scope2")
})
t.Run("Test having both Scope and ScopeArray", func(t *testing.T) {
c := hydrav1alpha1.OAuth2Client{
Spec: hydrav1alpha1.OAuth2ClientSpec{
Scope: "scope3",
ScopeArray: []string{"scope1", "scope2"},
},
}
var parsedClient, err = hydra.FromOAuth2Client(&c)
if err != nil {
assert.Fail(t, "unexpected error: %s", err)
}
assert.Equal(t, parsedClient.Scope, "scope1 scope2 scope3")
})
}