Skip to content

Commit 39d6d4e

Browse files
committed
extend e2e testcases
On-behalf-of: @SAP [email protected]
1 parent 5e8d9e2 commit 39d6d4e

File tree

2 files changed

+230
-63
lines changed

2 files changed

+230
-63
lines changed

test/e2e/authentication/mockoidc.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"crypto/x509"
2222
"net"
2323
"path/filepath"
24+
"sync"
2425
"testing"
2526

2627
"github.com/golang-jwt/jwt/v5"
@@ -74,6 +75,14 @@ func startMockOIDC(t *testing.T, server kcptestingserver.RunningServer) (*mockoi
7475
m, err := mockoidc.RunTLS(tlsConfig)
7576
require.NoError(t, err)
7677

78+
// Since most tests run in parallel, the main test will end sooner than the subtests and so
79+
// if we'd use `defer m.Shutdown()`, that defer would run before the subtests are even executed.
80+
// t.Cleanup() is a bit more picky and to avoid repetitive closures, this function sets up the
81+
// necessary shutdown code already.
82+
t.Cleanup(func() {
83+
require.NoError(t, m.Shutdown())
84+
})
85+
7786
return m, ca
7887
}
7988

@@ -102,6 +111,8 @@ func mockJWTAuthenticator(t *testing.T, m *mockoidc.MockOIDC, ca *crypto.CA) ten
102111
}
103112
}
104113

114+
var tokenLock sync.Mutex
115+
105116
func createOIDCToken(t *testing.T, mock *mockoidc.MockOIDC, subject, email string, groups []string) string {
106117
var (
107118
cfg = mock.Config()
@@ -112,6 +123,10 @@ func createOIDCToken(t *testing.T, mock *mockoidc.MockOIDC, subject, email strin
112123
codeChallengeMethod = cfg.CodeChallengeMethodsSupported[0]
113124
)
114125

126+
// NewSession() is not concurrency safe, but we want to allow parallel tests.
127+
tokenLock.Lock()
128+
defer tokenLock.Unlock()
129+
115130
session, err := mock.SessionStore.NewSession(scope, nonce, &mockoidc.MockUser{
116131
Subject: subject,
117132
Email: email,

test/e2e/authentication/workspace_test.go

Lines changed: 215 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ package authentication
1919
import (
2020
"context"
2121
"fmt"
22+
"math/rand"
2223
"testing"
2324
"time"
2425

2526
"github.com/stretchr/testify/require"
27+
"github.com/xrstf/mockoidc"
2628

2729
rbacv1 "k8s.io/api/rbac/v1"
2830
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -34,6 +36,7 @@ import (
3436
tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1"
3537
kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster"
3638
kcptesting "github.com/kcp-dev/kcp/sdk/testing"
39+
"github.com/kcp-dev/kcp/sdk/testing/third_party/library-go/crypto"
3740
"github.com/kcp-dev/kcp/test/e2e/framework"
3841
)
3942

@@ -53,49 +56,239 @@ func TestWorkspaceOIDC(t *testing.T) {
5356
kcpClusterClient, err := kcpclientset.NewForConfig(kcpConfig)
5457
require.NoError(t, err)
5558

56-
// start a mock OIDC server that will listen on a random port
59+
// start a two mock OIDC servers that will listen on random ports
5760
// (only for discovery and keyset handling, no actual login workflows)
58-
m, ca := startMockOIDC(t, server)
59-
defer m.Shutdown() //nolint:errcheck
61+
mockA, ca := startMockOIDC(t, server)
62+
mockB, _ := startMockOIDC(t, server)
63+
64+
// setup a new workspace auth config that uses mockoidc's server, one for
65+
// each of our mockoidc servers
66+
authConfigA := createWorkspaceAuthentication(t, ctx, kcpClusterClient, baseWsPath, mockA, ca)
67+
authConfigB := createWorkspaceAuthentication(t, ctx, kcpClusterClient, baseWsPath, mockB, ca)
68+
69+
// use these configs in new WorkspaceTypes and create one extra workspace type that allows
70+
// both mockoidc issuers
71+
wsTypeA := createWorkspaceType(t, ctx, kcpClusterClient, baseWsPath, authConfigA)
72+
wsTypeB := createWorkspaceType(t, ctx, kcpClusterClient, baseWsPath, authConfigB)
73+
wsTypeC := createWorkspaceType(t, ctx, kcpClusterClient, baseWsPath, authConfigA, authConfigB)
74+
75+
// create a new workspace with our new type
76+
t.Log("Creating Workspaces...")
77+
teamAPath, _ := kcptesting.NewWorkspaceFixture(t, server, baseWsPath, kcptesting.WithName("team-a"), kcptesting.WithType(baseWsPath, tenancyv1alpha1.WorkspaceTypeName(wsTypeA)))
78+
teamBPath, _ := kcptesting.NewWorkspaceFixture(t, server, baseWsPath, kcptesting.WithName("team-b"), kcptesting.WithType(baseWsPath, tenancyv1alpha1.WorkspaceTypeName(wsTypeB)))
79+
teamCPath, _ := kcptesting.NewWorkspaceFixture(t, server, baseWsPath, kcptesting.WithName("team-c"), kcptesting.WithType(baseWsPath, tenancyv1alpha1.WorkspaceTypeName(wsTypeC)))
80+
81+
// sanity check: owner can access their own workspaces
82+
for _, path := range []logicalcluster.Path{teamAPath, teamBPath, teamCPath} {
83+
t.Logf("The workspace owner should be allowed to list ConfigMaps in %s...", path)
84+
_, err = kubeClusterClient.Cluster(path).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{})
85+
require.NoError(t, err)
86+
}
87+
88+
// grant permissions to random users and groups
89+
grantWorkspaceAccess(t, ctx, kubeClusterClient, teamAPath, []rbacv1.Subject{{
90+
Kind: "User",
91+
Name: "oidc:[email protected]",
92+
}, {
93+
Kind: "Group",
94+
Name: "oidc:developers",
95+
}})
96+
97+
grantWorkspaceAccess(t, ctx, kubeClusterClient, teamBPath, []rbacv1.Subject{{
98+
Kind: "User",
99+
Name: "oidc:[email protected]",
100+
}, {
101+
Kind: "Group",
102+
Name: "oidc:testers",
103+
}, {
104+
Kind: "Group",
105+
Name: "oidc:developers",
106+
}})
107+
108+
grantWorkspaceAccess(t, ctx, kubeClusterClient, teamCPath, []rbacv1.Subject{{
109+
Kind: "User",
110+
Name: "oidc:[email protected]",
111+
}, {
112+
Kind: "Group",
113+
Name: "oidc:developers",
114+
}})
115+
116+
// random user with random token should have no access
117+
randoKubeClusterClient, err := kcpkubernetesclientset.NewForConfig(framework.ConfigWithToken("invalid-token", kcpConfig))
118+
require.NoError(t, err)
119+
120+
for _, path := range []logicalcluster.Path{teamAPath, teamBPath, teamCPath} {
121+
t.Logf("An unauthenticated user shouldn't be able to list ConfigMaps in %s...", path)
122+
123+
_, err = randoKubeClusterClient.Cluster(path).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{})
124+
require.Error(t, err)
125+
}
126+
127+
testcases := []struct {
128+
name string
129+
username string
130+
email string
131+
groups []string
132+
mock *mockoidc.MockOIDC
133+
workspaceAccess map[logicalcluster.Path]bool
134+
}{
135+
{
136+
name: "[email protected] should be able to access workspace A only",
137+
username: "user-a",
138+
139+
groups: nil,
140+
mock: mockA,
141+
workspaceAccess: map[logicalcluster.Path]bool{
142+
teamAPath: true,
143+
teamBPath: false,
144+
teamCPath: false, // user is authenticated but has no permissions in this workspace
145+
},
146+
},
147+
{
148+
name: "[email protected] should be able to access workspace B only",
149+
username: "user-b",
150+
151+
groups: nil,
152+
mock: mockB,
153+
workspaceAccess: map[logicalcluster.Path]bool{
154+
teamAPath: false,
155+
teamBPath: true,
156+
teamCPath: false, // user is authenticated but has no permissions in this workspace
157+
},
158+
},
159+
{
160+
name: "[email protected] (developers) should be able to access workspace A and C",
161+
username: "user-a",
162+
163+
groups: []string{"developers"},
164+
mock: mockA,
165+
workspaceAccess: map[logicalcluster.Path]bool{
166+
teamAPath: true,
167+
teamBPath: false,
168+
teamCPath: true,
169+
},
170+
},
171+
{
172+
name: "Any user in the developers group should be able to access workspace A and C",
173+
username: "random-user",
174+
175+
groups: []string{"developers"},
176+
mock: mockA,
177+
workspaceAccess: map[logicalcluster.Path]bool{
178+
teamAPath: true,
179+
teamBPath: false, // false is correct since B does allow developers in, but only from its own issuer!
180+
teamCPath: true,
181+
},
182+
},
183+
{
184+
name: "User C, signed by issuer A, is allowed to access workspace C",
185+
username: "user-c",
186+
187+
groups: nil,
188+
mock: mockA,
189+
workspaceAccess: map[logicalcluster.Path]bool{
190+
teamAPath: false,
191+
teamBPath: false,
192+
teamCPath: true,
193+
},
194+
},
195+
{
196+
name: "User C, signed by issuer B, is allowed to access workspace C",
197+
username: "user-c",
198+
199+
groups: nil,
200+
mock: mockB,
201+
workspaceAccess: map[logicalcluster.Path]bool{
202+
teamAPath: false,
203+
teamBPath: false,
204+
teamCPath: true,
205+
},
206+
},
207+
}
208+
209+
for _, testcase := range testcases {
210+
t.Run(testcase.name, func(t *testing.T) {
211+
t.Parallel()
212+
213+
token := createOIDCToken(t, testcase.mock, testcase.username, testcase.email, testcase.groups)
214+
215+
client, err := kcpkubernetesclientset.NewForConfig(framework.ConfigWithToken(token, kcpConfig))
216+
require.NoError(t, err)
217+
218+
for workspace, expectedResult := range testcase.workspaceAccess {
219+
t.Run(workspace.Base(), func(t *testing.T) {
220+
t.Parallel()
221+
222+
if expectedResult {
223+
// Initialization of the JWT authenticator happens asynchronously and depends both
224+
// on the cluster index knowing the WorkspaceType and the authenticator itself
225+
// performing OIDC discovery.
226+
227+
require.Eventually(t, func() bool {
228+
_, err := client.Cluster(workspace).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{})
229+
230+
return err == nil
231+
}, wait.ForeverTestTimeout, 500*time.Millisecond)
232+
} else {
233+
_, err := client.Cluster(workspace).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{})
234+
require.Error(t, err, "user should have no access")
235+
}
236+
})
237+
}
238+
})
239+
}
240+
}
241+
242+
func createWorkspaceAuthentication(t *testing.T, ctx context.Context, client kcpclientset.ClusterInterface, workspace logicalcluster.Path, mock *mockoidc.MockOIDC, ca *crypto.CA) string {
243+
name := fmt.Sprintf("mockoidc-%d", rand.Int())
60244

61245
// setup a new workspace auth config that uses mockoidc's server
62246
authConfig := &tenancyv1alpha1.WorkspaceAuthenticationConfiguration{
63247
ObjectMeta: metav1.ObjectMeta{
64-
Name: "mockoidc",
248+
Name: name,
65249
},
66250
Spec: tenancyv1alpha1.WorkspaceAuthenticationConfigurationSpec{
67251
JWT: []tenancyv1alpha1.JWTAuthenticator{
68-
mockJWTAuthenticator(t, m, ca),
252+
mockJWTAuthenticator(t, mock, ca),
69253
},
70254
},
71255
}
72256

73-
t.Log("Creating WorkspaceAuthenticationConfguration...")
74-
_, err = kcpClusterClient.Cluster(baseWsPath).TenancyV1alpha1().WorkspaceAuthenticationConfigurations().Create(ctx, authConfig, metav1.CreateOptions{})
257+
t.Logf("Creating WorkspaceAuthenticationConfguration %s...", name)
258+
_, err := client.Cluster(workspace).TenancyV1alpha1().WorkspaceAuthenticationConfigurations().Create(ctx, authConfig, metav1.CreateOptions{})
75259
require.NoError(t, err)
76260

77-
// use this config in a new WorkspaceType
261+
return name
262+
}
263+
264+
func createWorkspaceType(t *testing.T, ctx context.Context, client kcpclientset.ClusterInterface, workspace logicalcluster.Path, authConfigNames ...string) string {
265+
name := fmt.Sprintf("with-oidc-%d", rand.Int())
266+
267+
configs := []tenancyv1alpha1.AuthenticationConfigurationReference{}
268+
for _, name := range authConfigNames {
269+
configs = append(configs, tenancyv1alpha1.AuthenticationConfigurationReference{
270+
Name: name,
271+
})
272+
}
273+
274+
// setup a new workspace auth config that uses mockoidc's server
78275
wsType := &tenancyv1alpha1.WorkspaceType{
79276
ObjectMeta: metav1.ObjectMeta{
80-
Name: "with-oidc",
277+
Name: name,
81278
},
82279
Spec: tenancyv1alpha1.WorkspaceTypeSpec{
83-
AuthenticationConfigurations: []tenancyv1alpha1.AuthenticationConfigurationReference{{
84-
Name: authConfig.Name,
85-
}},
280+
AuthenticationConfigurations: configs,
86281
},
87282
}
88283

89-
t.Log("Creating WorkspaceType...")
90-
_, err = kcpClusterClient.Cluster(baseWsPath).TenancyV1alpha1().WorkspaceTypes().Create(ctx, wsType, metav1.CreateOptions{})
284+
t.Logf("Creating WorkspaceType %s...", name)
285+
_, err := client.Cluster(workspace).TenancyV1alpha1().WorkspaceTypes().Create(ctx, wsType, metav1.CreateOptions{})
91286
require.NoError(t, err)
92287

93-
// create a new workspace with our new type
94-
t.Log("Creating Workspace...")
95-
team1Path, _ := kcptesting.NewWorkspaceFixture(t, server, baseWsPath, kcptesting.WithName("team1"), kcptesting.WithType(baseWsPath, tenancyv1alpha1.WorkspaceTypeName(wsType.Name)))
288+
return name
289+
}
96290

97-
// grant permissions to OIDC user
98-
email := "[email protected]"
291+
func grantWorkspaceAccess(t *testing.T, ctx context.Context, client kcpkubernetesclientset.ClusterInterface, workspace logicalcluster.Path, subjects []rbacv1.Subject) {
99292
crb := &rbacv1.ClusterRoleBinding{
100293
ObjectMeta: metav1.ObjectMeta{
101294
Name: "allow-oidc-user",
@@ -104,51 +297,10 @@ func TestWorkspaceOIDC(t *testing.T) {
104297
Kind: "ClusterRole",
105298
Name: "cluster-admin",
106299
},
107-
Subjects: []rbacv1.Subject{{
108-
Kind: "User",
109-
Name: fmt.Sprintf("oidc:%s", email),
110-
}, {
111-
Kind: "Group",
112-
Name: "oidc:developers",
113-
}},
300+
Subjects: subjects,
114301
}
115302

116-
t.Log("Grating OIDC permissions...")
117-
_, err = kubeClusterClient.Cluster(team1Path).RbacV1().ClusterRoleBindings().Create(ctx, crb, metav1.CreateOptions{})
118-
require.NoError(t, err)
119-
120-
// sanity check: owner can access their own workspace
121-
t.Log("Owner should be allowed to list ConfigMaps.")
122-
_, err = kubeClusterClient.Cluster(team1Path).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{})
123-
require.NoError(t, err)
124-
125-
// random user with random token should have no access
126-
randoKubeClusterClient, err := kcpkubernetesclientset.NewForConfig(framework.ConfigWithToken("invalid-token", kcpConfig))
303+
t.Log("Creating ClusterRoleBinding...")
304+
_, err := client.Cluster(workspace).RbacV1().ClusterRoleBindings().Create(ctx, crb, metav1.CreateOptions{})
127305
require.NoError(t, err)
128-
129-
t.Log("An unauthenticated user shouldn't be able to list ConfigMaps.")
130-
_, err = randoKubeClusterClient.Cluster(team1Path).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{})
131-
require.Error(t, err)
132-
133-
// A user with a valid token from the issuer however should be allowed in.
134-
// Test that both group and user claims work as expected.
135-
adminToken := createOIDCToken(t, m, "the-admin", email, []string{"admins"})
136-
devToken := createOIDCToken(t, m, "the-dev", "[email protected]", []string{"developers"})
137-
138-
t.Logf("OIDC-authenticated users should be able to list ConfigMaps.")
139-
140-
for _, token := range []string{adminToken, devToken} {
141-
authenticatedKubeClusterClient, err := kcpkubernetesclientset.NewForConfig(framework.ConfigWithToken(token, kcpConfig))
142-
require.NoError(t, err)
143-
144-
// Initialization of the JWT authenticator happens asynchronously and depends both
145-
// on the cluster index knowing the WorkspaceType and the authenticator itself
146-
// performing OIDC discovery.
147-
148-
require.Eventually(t, func() bool {
149-
_, err := authenticatedKubeClusterClient.Cluster(team1Path).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{})
150-
151-
return err == nil
152-
}, wait.ForeverTestTimeout, 500*time.Millisecond)
153-
}
154306
}

0 commit comments

Comments
 (0)