Skip to content

Commit 8c56e4a

Browse files
committed
Test service account RBAC too
Broadly, these are the same as users, but ServiceAccounts can only have permissions in their parent org, so that's checked explicitly. Creating service accounts needs an issuer, and this is a faff to set up; so I have recreated what the handler does, by using `conversion.NewObjectMetadata(...)`. This means the accounts get a fresh UID every time, so the IDs get wrapped in a hold-all fixture. (Usually this is a good idea anyway, because it means you can generate names and IDs, and check everything still works.)
1 parent 5da8106 commit 8c56e4a

File tree

1 file changed

+160
-10
lines changed

1 file changed

+160
-10
lines changed

pkg/rbac/groups_test.go

Lines changed: 160 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ package rbac_test
1919
import (
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+
4456
const (
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
320405
func 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

Comments
 (0)