@@ -19,12 +19,14 @@ package rbac_test
1919import (
2020 "context"
2121 "testing"
22+ "time"
2223
2324 "github.com/stretchr/testify/assert"
2425 "github.com/stretchr/testify/require"
2526
2627 "github.com/unikorn-cloud/core/pkg/constants"
2728 coreopenapi "github.com/unikorn-cloud/core/pkg/openapi"
29+ "github.com/unikorn-cloud/core/pkg/server/conversion"
2830 unikornv1 "github.com/unikorn-cloud/identity/pkg/apis/unikorn/v1alpha1"
2931 handlercommon "github.com/unikorn-cloud/identity/pkg/handler/common"
3032 "github.com/unikorn-cloud/identity/pkg/handler/users"
@@ -36,16 +38,29 @@ import (
3638 corev1 "k8s.io/api/core/v1"
3739 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3840 "k8s.io/apimachinery/pkg/runtime"
41+ "k8s.io/utils/ptr"
3942
4043 "sigs.k8s.io/controller-runtime/pkg/client"
4144 "sigs.k8s.io/controller-runtime/pkg/client/fake"
4245)
4346
47+ type fixture struct {
48+ // Because of the way service account creation works, we can't specify the ID
49+ // for a service account when creating it; we have to create it then look at
50+ // the ID it was given.
51+ serviceAccountAlphaID , serviceAccountBetaID , serviceAccountAltAlphaID string
52+
53+ rbac * rbac.RBAC
54+ }
55+
4456const (
4557 testNamespace = "test-namespace"
4658 testOrgID = "test-org"
4759 testOrgNS = "test-org-ns"
4860
61+ altOrgID = "alt-org-id"
62+ altOrgNS = "alt-namespace"
63+
4964 userAliceSubject = "alice@example.com"
5065 userAliceID = "user-alice"
5166
@@ -55,9 +70,13 @@ const (
5570 userCharlieSubject = "charlie@example.com"
5671 userCharlieID = "user-charlie"
5772
73+ serviceAccountAlphaName = "sa-alpha"
74+ serviceAccountBetaName = "sa-beta"
75+
5876 groupAdminsID = "group-admins"
5977 groupDevelopers = "group-developers"
6078 groupReaders = "group-readers"
79+ groupServices = "group-services"
6180
6281 roleAdminID = "role-admin"
6382 roleDeveloperID = "role-developer"
@@ -129,10 +148,35 @@ func newOrganization(objectNamespace, name, orgNamespace string) *unikornv1.Orga
129148 }
130149}
131150
151+ func createServiceAccount (t * testing.T , c client.Client , name , orgID , orgNamespace string ) string {
152+ t .Helper ()
153+
154+ // It would be better to use the API, but creating service accounts needs a token issuer,
155+ // and that is a pain to set up. So: fake it by creating a record in Kubernetes the way
156+ // the handler would, rather than going through the API.
157+ meta := & coreopenapi.ResourceWriteMetadata {
158+ Name : name ,
159+ }
160+
161+ objectMeta := conversion .NewObjectMetadata (meta , orgNamespace ).WithOrganization (orgID ).Get ()
162+ sa := unikornv1.ServiceAccount {
163+ ObjectMeta : objectMeta ,
164+ Spec : unikornv1.ServiceAccountSpec {
165+ Expiry : ptr .To (metav1 .NewTime (time .Now ().Add (2 * time .Hour ))),
166+ },
167+ }
168+
169+ require .NoError (t , c .Create (t .Context (), & sa ))
170+
171+ return sa .Name // hereafter used as the service account ID
172+ }
173+
132174// setupTestEnvironment creates a comprehensive RBAC test environment with users, groups, roles, and projects.
133- func setupTestEnvironment (t * testing.T ) (client. Client , * rbac. RBAC ) {
175+ func setupTestEnvironment (t * testing.T ) fixture {
134176 t .Helper ()
135177
178+ var f fixture
179+
136180 scheme := runtime .NewScheme ()
137181 require .NoError (t , corev1 .AddToScheme (scheme ))
138182 require .NoError (t , unikornv1 .AddToScheme (scheme ))
@@ -149,7 +193,18 @@ func setupTestEnvironment(t *testing.T) (client.Client, *rbac.RBAC) {
149193
150194 organization := newOrganization (testNamespace , testOrgID , testOrgNS )
151195
152- createObjects (organization )
196+ altOrganization := & unikornv1.Organization {
197+ ObjectMeta : metav1.ObjectMeta {
198+ Namespace : testNamespace ,
199+ Name : altOrgID ,
200+ },
201+ Spec : unikornv1.OrganizationSpec {},
202+ Status : unikornv1.OrganizationStatus {
203+ Namespace : altOrgNS ,
204+ },
205+ }
206+
207+ createObjects (organization , altOrganization )
153208 require .NoError (t , c .Update (t .Context (), organization )) // to update the status with a namespace.
154209
155210 // Create Roles with different permission scopes.
@@ -255,7 +310,36 @@ func setupTestEnvironment(t *testing.T) (client.Client, *rbac.RBAC) {
255310 },
256311 }
257312
258- createObjects (groupAdminsObj , groupReadersObj , groupDevelopersObj )
313+ f .serviceAccountAlphaID = createServiceAccount (t , c , serviceAccountAlphaName , testOrgID , testOrgNS )
314+ f .serviceAccountBetaID = createServiceAccount (t , c , serviceAccountBetaName , testOrgID , testOrgNS )
315+ f .serviceAccountAltAlphaID = createServiceAccount (t , c , serviceAccountAlphaName , altOrgID , altOrgNS )
316+
317+ // Group for service accounts
318+ groupServicesObj := & unikornv1.Group {
319+ ObjectMeta : metav1.ObjectMeta {
320+ Namespace : testOrgNS ,
321+ Name : groupServices ,
322+ },
323+ Spec : unikornv1.GroupSpec {
324+ RoleIDs : []string {roleDeveloperID },
325+ ServiceAccountIDs : []string {f .serviceAccountAlphaID },
326+ },
327+ }
328+
329+ // Group in alternate org for the service account there, to check the calculation for Org A does not
330+ // include permissions from Org B.
331+ groupAltServicesObj := & unikornv1.Group {
332+ ObjectMeta : metav1.ObjectMeta {
333+ Namespace : altOrgNS ,
334+ Name : groupServices ,
335+ },
336+ Spec : unikornv1.GroupSpec {
337+ RoleIDs : []string {roleDeveloperID },
338+ ServiceAccountIDs : []string {f .serviceAccountAltAlphaID },
339+ },
340+ }
341+
342+ createObjects (groupAdminsObj , groupReadersObj , groupDevelopersObj , groupServicesObj , groupAltServicesObj )
259343
260344 projectAlpha := & unikornv1.Project {
261345 ObjectMeta : metav1.ObjectMeta {
@@ -266,7 +350,7 @@ func setupTestEnvironment(t *testing.T) (client.Client, *rbac.RBAC) {
266350 },
267351 },
268352 Spec : unikornv1.ProjectSpec {
269- GroupIDs : []string {groupDevelopers }, // Only developers have access
353+ GroupIDs : []string {groupDevelopers , groupServices }, // Developers and services have access
270354 },
271355 }
272356
@@ -279,7 +363,7 @@ func setupTestEnvironment(t *testing.T) (client.Client, *rbac.RBAC) {
279363 },
280364 },
281365 Spec : unikornv1.ProjectSpec {
282- GroupIDs : []string {groupDevelopers , groupReaders }, // Developers and readers
366+ GroupIDs : []string {groupDevelopers , groupReaders , groupServices }, // Developers, readers, and services
283367 },
284368 }
285369
@@ -290,8 +374,9 @@ func setupTestEnvironment(t *testing.T) (client.Client, *rbac.RBAC) {
290374 createUser (t , c , userCharlieID , userCharlieSubject , []* unikornv1.Group {groupReadersObj , groupDevelopersObj })
291375
292376 rbacClient := rbac .New (c , testNamespace , & rbac.Options {})
377+ f .rbac = rbacClient
293378
294- return c , rbacClient
379+ return f
295380}
296381
297382// getACLForUser is a helper to get the ACL for a given user subject.
@@ -320,16 +405,16 @@ func getACLForUser(t *testing.T, rbacClient *rbac.RBAC, subject string) *openapi
320405func TestGroupACLContent (t * testing.T ) {
321406 t .Parallel ()
322407
323- _ , rbacClient := setupTestEnvironment (t )
408+ f := setupTestEnvironment (t )
324409
325410 // Test Alice (Admin) - should have organization permissions.
326- aclAlice := getACLForUser (t , rbacClient , userAliceSubject )
411+ aclAlice := getACLForUser (t , f . rbac , userAliceSubject )
327412 assert .Nil (t , aclAlice .Global , "Alice should not have global permissions" )
328413 assert .NotNil (t , aclAlice .Organization , "Alice should have organization permissions" )
329414 assert .Nil (t , aclAlice .Projects , "Alice should not have project-specific permissions" )
330415
331416 // Test Bob (Developer) - should have organization read and project permissions.
332- aclBob := getACLForUser (t , rbacClient , userBobSubject )
417+ aclBob := getACLForUser (t , f . rbac , userBobSubject )
333418 assert .Nil (t , aclBob .Global , "Bob should not have global permissions" )
334419 assert .NotNil (t , aclBob .Organization , "Bob should have organization permissions" )
335420 assert .NotNil (t , aclBob .Projects , "Bob should have project permissions" )
@@ -349,7 +434,7 @@ func TestGroupACLContent(t *testing.T) {
349434 assert .True (t , hasOrgRead , "Bob should have org:read permission" )
350435
351436 // Test Charlie (Developer + Reader) - should have merged permissions.
352- aclCharlie := getACLForUser (t , rbacClient , userCharlieSubject )
437+ aclCharlie := getACLForUser (t , f . rbac , userCharlieSubject )
353438 assert .Nil (t , aclCharlie .Global , "Charlie should not have global permissions" )
354439 assert .NotNil (t , aclCharlie .Organization , "Charlie should have organization permissions" )
355440 assert .NotNil (t , aclCharlie .Projects , "Charlie should have project permissions" )
@@ -390,3 +475,68 @@ func TestGroupACLContent(t *testing.T) {
390475 }
391476 }
392477}
478+
479+ // getACLForServiceAccount is a helper to get the ACL for a given service account.
480+ func getACLForServiceAccount (t * testing.T , rbacClient * rbac.RBAC , subject string ) * openapi.Acl {
481+ t .Helper ()
482+
483+ // Create authorization info for service account
484+ info := & authorization.Info {
485+ Userinfo : & openapi.Userinfo {
486+ Sub : subject ,
487+ },
488+ ServiceAccount : true ,
489+ }
490+
491+ ctx := authorization .NewContext (t .Context (), info )
492+
493+ acl , err := rbacClient .GetACL (ctx , testOrgID )
494+ require .NoError (t , err )
495+ require .NotNil (t , acl )
496+
497+ return acl
498+ }
499+
500+ // TestServiceAccountACL verifies that service accounts get correct permissions via groups.
501+ func TestServiceAccountACL (t * testing.T ) {
502+ t .Parallel ()
503+
504+ f := setupTestEnvironment (t )
505+
506+ // Test service account that's a member of the services group
507+ aclAlpha := getACLForServiceAccount (t , f .rbac , f .serviceAccountAlphaID )
508+ assert .Nil (t , aclAlpha .Global , "Service account should not have global permissions" )
509+ assert .NotNil (t , aclAlpha .Organization , "Service account should have organization permissions" )
510+ assert .NotNil (t , aclAlpha .Projects , "Service account should have project permissions" )
511+
512+ // Verify service account has org:read (from developer role)
513+ hasOrgRead := false
514+
515+ for _ , endpoint := range aclAlpha .Organization .Endpoints {
516+ if endpoint .Name == "org:read" {
517+ hasOrgRead = true
518+
519+ require .Contains (t , endpoint .Operations , openapi .Read )
520+ }
521+ }
522+
523+ assert .True (t , hasOrgRead , "Service account should have org:read permission" )
524+
525+ // Service account should have access to projects (from developer role)
526+ assert .Len (t , * aclAlpha .Projects , 2 , "Service account should have access to 2 projects" )
527+
528+ // Test service account not in any group
529+ aclBeta := getACLForServiceAccount (t , f .rbac , f .serviceAccountBetaID )
530+ assert .Nil (t , aclBeta .Global , "Service account not in groups should not have global permissions" )
531+ assert .Nil (t , aclBeta .Organization , "Service account not in groups should not have organization permissions" )
532+ assert .Nil (t , aclBeta .Projects , "Service account not in groups should not have project permissions" )
533+ }
534+
535+ func TestServiceAccount_WrongOrganization (t * testing.T ) {
536+ t .Parallel ()
537+
538+ f := setupTestEnvironment (t )
539+
540+ aclAlpha := getACLForServiceAccount (t , f .rbac , f .serviceAccountAltAlphaID )
541+ assert .Empty (t , aclAlpha , "Service account bound to org B should have no permissions in org A" )
542+ }
0 commit comments