@@ -19,10 +19,12 @@ package authentication
1919import (
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+ 92+ }, {
93+ Kind : "Group" ,
94+ Name : "oidc:developers" ,
95+ }})
96+
97+ grantWorkspaceAccess (t , ctx , kubeClusterClient , teamBPath , []rbacv1.Subject {{
98+ Kind : "User" ,
99+ 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+ 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- 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