Skip to content
This repository was archived by the owner on Jun 23, 2026. It is now read-only.

Commit ada2899

Browse files
feat: add --additional-audiences flag for workspace JWT authentication (#471)
Allow configuring extra trusted audiences in the WorkspaceAuthenticationConfiguration beyond those derived from OIDC clients in AccountInfo. On-behalf-of: @SAP <bastian.echterhoelter@sap.com> Signed-off-by: Bastian Echterhölter <bastian.echterhoelter@sap.com> Co-authored-by: Aaron Schweig <42006873+aaronschweig@users.noreply.github.com>
1 parent ce5762a commit ada2899

4 files changed

Lines changed: 110 additions & 1 deletion

File tree

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ type Config struct {
8484
Keycloak KeycloakConfig
8585
Initializer InitializerConfig
8686
Webhooks WebhooksConfig
87+
AdditionalAudiences []string
8788
}
8889

8990
func NewConfig() Config {
@@ -167,6 +168,7 @@ func (c *Config) AddFlags(fs *pflag.FlagSet) {
167168
fs.BoolVar(&c.Initializer.IDPEnabled, "initializer-idp-enabled", c.Initializer.IDPEnabled, "Enable IDP initialization")
168169
fs.BoolVar(&c.Initializer.InviteEnabled, "initializer-invite-enabled", c.Initializer.InviteEnabled, "Enable invite initialization")
169170
fs.BoolVar(&c.Initializer.WorkspaceAuthEnabled, "initializer-workspace-auth-enabled", c.Initializer.WorkspaceAuthEnabled, "Enable workspace auth initialization")
171+
fs.StringSliceVar(&c.AdditionalAudiences, "additional-audiences", c.AdditionalAudiences, "Additional audiences to trust in workspace JWT authentication configurations")
170172
fs.BoolVar(&c.Webhooks.Enabled, "webhooks-enabled", c.Webhooks.Enabled, "Enable validating webhooks")
171173
fs.IntVar(&c.Webhooks.Port, "webhooks-port", c.Webhooks.Port, "Set webhook server port")
172174
fs.StringVar(&c.Webhooks.CertDir, "webhooks-cert-dir", c.Webhooks.CertDir, "Set webhook certificate directory")

internal/config/config_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func TestNewConfig(t *testing.T) {
1717
assert.Equal(t, "security-operator", cfg.Keycloak.ClientID)
1818
assert.Equal(t, 9443, cfg.Webhooks.Port)
1919
assert.Equal(t, []string{"http://localhost:8000", "http://localhost:18000"}, cfg.IDP.KubectlClientRedirectURLs)
20+
assert.Nil(t, cfg.AdditionalAudiences)
2021
}
2122

2223
func TestConfigAddFlags(t *testing.T) {
@@ -30,6 +31,7 @@ func TestConfigAddFlags(t *testing.T) {
3031
"--idp-kubectl-client-redirect-urls=http://localhost:7000,http://localhost:17000",
3132
"--webhooks-enabled=true",
3233
"--webhooks-port=10443",
34+
"--additional-audiences=aud-a,aud-b",
3335
})
3436

3537
assert.NoError(t, err)
@@ -38,6 +40,7 @@ func TestConfigAddFlags(t *testing.T) {
3840
assert.Equal(t, []string{"http://localhost:7000", "http://localhost:17000"}, cfg.IDP.KubectlClientRedirectURLs)
3941
assert.True(t, cfg.Webhooks.Enabled)
4042
assert.Equal(t, 10443, cfg.Webhooks.Port)
43+
assert.Equal(t, []string{"aud-a", "aud-b"}, cfg.AdditionalAudiences)
4144
}
4245

4346
func TestInitContainerConfigAddFlags(t *testing.T) {

internal/subroutine/workspace_authorization.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,14 @@ func (r *workspaceAuthSubroutine) reconcile(ctx context.Context, obj client.Obje
8686
return subroutines.OK(), fmt.Errorf("AccountInfo %s has no OIDC clients", workspaceName)
8787
}
8888

89-
audiences := make([]string, 0, len(accountInfo.Spec.OIDC.Clients))
89+
audiences := make([]string, 0, len(accountInfo.Spec.OIDC.Clients)+len(r.cfg.AdditionalAudiences))
9090
for clientName, clientInfo := range accountInfo.Spec.OIDC.Clients {
9191
if clientInfo.ClientID == "" {
9292
return subroutines.OK(), fmt.Errorf("OIDC client %s has empty ClientID in AccountInfo", clientName)
9393
}
9494
audiences = append(audiences, clientInfo.ClientID)
9595
}
96+
audiences = append(audiences, r.cfg.AdditionalAudiences...)
9697

9798
jwtAuthenticationConfiguration := kcptenancyv1alphav1.JWTAuthenticator{
9899
Issuer: kcptenancyv1alphav1.Issuer{

internal/subroutine/workspace_authorization_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,109 @@ func TestWorkspaceAuthSubroutine_Initialize(t *testing.T) {
679679
expectError: true,
680680
expectedResult: subroutines.OK(),
681681
},
682+
{
683+
name: "success - additional audiences are appended",
684+
logicalCluster: &kcpcorev1alpha1.LogicalCluster{
685+
ObjectMeta: metav1.ObjectMeta{
686+
Annotations: map[string]string{
687+
"kcp.io/path": "root:orgs:test-workspace",
688+
},
689+
},
690+
},
691+
cfg: config.Config{
692+
BaseDomain: "test.domain",
693+
GroupClaim: "groups",
694+
UserClaim: "email",
695+
AdditionalAudiences: []string{"extra-aud-1", "extra-aud-2"},
696+
},
697+
setupMocks: func(m *mocks.MockClient, mgrClient *mocks.MockClient) {
698+
mgrClient.EXPECT().Get(mock.Anything, types.NamespacedName{Name: "account"}, mock.AnythingOfType("*v1alpha1.AccountInfo"), mock.Anything).
699+
RunAndReturn(func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
700+
*obj.(*accountsv1alpha1.AccountInfo) = accountsv1alpha1.AccountInfo{
701+
ObjectMeta: metav1.ObjectMeta{Name: "account"},
702+
Spec: accountsv1alpha1.AccountInfoSpec{
703+
OIDC: &accountsv1alpha1.OIDCInfo{
704+
Clients: map[string]accountsv1alpha1.ClientInfo{
705+
"test-workspace": {ClientID: "test-workspace-client"},
706+
"kubectl": {ClientID: "kubectl-client"},
707+
},
708+
},
709+
},
710+
}
711+
return nil
712+
}).Once()
713+
m.EXPECT().Get(mock.Anything, types.NamespacedName{Name: "test-workspace"}, mock.AnythingOfType("*v1alpha1.WorkspaceAuthenticationConfiguration"), mock.Anything).
714+
Return(apierrors.NewNotFound(kcptenancyv1alphav1.Resource("workspaceauthenticationconfigurations"), "test-workspace")).Once()
715+
m.EXPECT().Create(mock.Anything, mock.AnythingOfType("*v1alpha1.WorkspaceAuthenticationConfiguration"), mock.Anything).
716+
RunAndReturn(func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
717+
wac := obj.(*kcptenancyv1alphav1.WorkspaceAuthenticationConfiguration)
718+
assert.Equal(t, "test-workspace", wac.Name)
719+
assert.ElementsMatch(t, []string{"test-workspace-client", "kubectl-client", "extra-aud-1", "extra-aud-2"}, wac.Spec.JWT[0].Issuer.Audiences)
720+
return nil
721+
}).Once()
722+
723+
m.EXPECT().List(mock.Anything, mock.AnythingOfType("*v1alpha1.WorkspaceTypeList"), mock.Anything).
724+
RunAndReturn(func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
725+
wtList := list.(*kcptenancyv1alphav1.WorkspaceTypeList)
726+
wtList.Items = []kcptenancyv1alphav1.WorkspaceType{}
727+
return nil
728+
}).Once()
729+
},
730+
expectError: false,
731+
expectedResult: subroutines.OK(),
732+
},
733+
{
734+
name: "success - empty additional audiences does not change behavior",
735+
logicalCluster: &kcpcorev1alpha1.LogicalCluster{
736+
ObjectMeta: metav1.ObjectMeta{
737+
Annotations: map[string]string{
738+
"kcp.io/path": "root:orgs:test-workspace",
739+
},
740+
},
741+
},
742+
cfg: config.Config{
743+
BaseDomain: "test.domain",
744+
GroupClaim: "groups",
745+
UserClaim: "email",
746+
AdditionalAudiences: []string{},
747+
},
748+
setupMocks: func(m *mocks.MockClient, mgrClient *mocks.MockClient) {
749+
mgrClient.EXPECT().Get(mock.Anything, types.NamespacedName{Name: "account"}, mock.AnythingOfType("*v1alpha1.AccountInfo"), mock.Anything).
750+
RunAndReturn(func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
751+
*obj.(*accountsv1alpha1.AccountInfo) = accountsv1alpha1.AccountInfo{
752+
ObjectMeta: metav1.ObjectMeta{Name: "account"},
753+
Spec: accountsv1alpha1.AccountInfoSpec{
754+
OIDC: &accountsv1alpha1.OIDCInfo{
755+
Clients: map[string]accountsv1alpha1.ClientInfo{
756+
"test-workspace": {ClientID: "test-workspace-client"},
757+
"kubectl": {ClientID: "kubectl-client"},
758+
},
759+
},
760+
},
761+
}
762+
return nil
763+
}).Once()
764+
m.EXPECT().Get(mock.Anything, types.NamespacedName{Name: "test-workspace"}, mock.AnythingOfType("*v1alpha1.WorkspaceAuthenticationConfiguration"), mock.Anything).
765+
Return(apierrors.NewNotFound(kcptenancyv1alphav1.Resource("workspaceauthenticationconfigurations"), "test-workspace")).Once()
766+
m.EXPECT().Create(mock.Anything, mock.AnythingOfType("*v1alpha1.WorkspaceAuthenticationConfiguration"), mock.Anything).
767+
RunAndReturn(func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
768+
wac := obj.(*kcptenancyv1alphav1.WorkspaceAuthenticationConfiguration)
769+
assert.Equal(t, "test-workspace", wac.Name)
770+
assert.ElementsMatch(t, []string{"test-workspace-client", "kubectl-client"}, wac.Spec.JWT[0].Issuer.Audiences)
771+
assert.Len(t, wac.Spec.JWT[0].Issuer.Audiences, 2)
772+
return nil
773+
}).Once()
774+
775+
m.EXPECT().List(mock.Anything, mock.AnythingOfType("*v1alpha1.WorkspaceTypeList"), mock.Anything).
776+
RunAndReturn(func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
777+
wtList := list.(*kcptenancyv1alphav1.WorkspaceTypeList)
778+
wtList.Items = []kcptenancyv1alphav1.WorkspaceType{}
779+
return nil
780+
}).Once()
781+
},
782+
expectError: false,
783+
expectedResult: subroutines.OK(),
784+
},
682785
}
683786

684787
for _, tt := range tests {

0 commit comments

Comments
 (0)