@@ -4,6 +4,7 @@ package tenantnamespace
44
55import (
66 "context"
7+ "errors"
78 "testing"
89
910 corev1 "k8s.io/api/core/v1"
@@ -14,7 +15,9 @@ import (
1415 "k8s.io/apimachinery/pkg/runtime/schema"
1516 "k8s.io/apiserver/pkg/authentication/user"
1617 "k8s.io/apiserver/pkg/endpoints/request"
18+ "sigs.k8s.io/controller-runtime/pkg/client"
1719 "sigs.k8s.io/controller-runtime/pkg/client/fake"
20+ "sigs.k8s.io/controller-runtime/pkg/client/interceptor"
1821
1922 corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
2023)
@@ -267,6 +270,290 @@ func TestHasAccessToNamespace_CozyAdminGroup(t *testing.T) {
267270 }
268271}
269272
273+ // TestList_FiltersUnauthorizedNamespaces asserts the List path returns only
274+ // tenant namespaces the user can access: RoleBindings for other users don't
275+ // grant anything, and multiple matching RoleBindings in one namespace don't
276+ // duplicate it in the result.
277+ func TestList_FiltersUnauthorizedNamespaces (t * testing.T ) {
278+ roleRef := rbacv1.RoleRef {APIGroup : "rbac.authorization.k8s.io" , Kind : "Role" , Name : "test-role" }
279+ client := fake .NewClientBuilder ().
280+ WithScheme (newTestScheme (t )).
281+ WithObjects (
282+ & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : "tenant-allowed" }},
283+ & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : "tenant-denied" }},
284+ & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : "kube-system" }},
285+ & rbacv1.RoleBinding {
286+ ObjectMeta : metav1.ObjectMeta {Name : "user-binding" , Namespace : "tenant-allowed" },
287+ Subjects : []rbacv1.Subject {{Kind : "User" , Name : "test-user" , APIGroup : "rbac.authorization.k8s.io" }},
288+ RoleRef : roleRef ,
289+ },
290+ & rbacv1.RoleBinding {
291+ ObjectMeta : metav1.ObjectMeta {Name : "group-binding" , Namespace : "tenant-allowed" },
292+ Subjects : []rbacv1.Subject {{Kind : "Group" , Name : "system:authenticated" , APIGroup : "rbac.authorization.k8s.io" }},
293+ RoleRef : roleRef ,
294+ },
295+ & rbacv1.RoleBinding {
296+ ObjectMeta : metav1.ObjectMeta {Name : "other-binding" , Namespace : "tenant-denied" },
297+ Subjects : []rbacv1.Subject {{Kind : "User" , Name : "other-user" , APIGroup : "rbac.authorization.k8s.io" }},
298+ RoleRef : roleRef ,
299+ },
300+ ).
301+ Build ()
302+
303+ r := & REST {
304+ c : client ,
305+ gvr : schema.GroupVersionResource {Group : "core.cozystack.io" , Version : "v1alpha1" , Resource : "tenantnamespaces" },
306+ }
307+
308+ u := & user.DefaultInfo {Name : "test-user" , Groups : []string {"system:authenticated" }}
309+ ctx := request .WithUser (context .Background (), u )
310+
311+ obj , err := r .List (ctx , nil )
312+ if err != nil {
313+ t .Fatalf ("unexpected error: %v" , err )
314+ }
315+ list , ok := obj .(* corev1alpha1.TenantNamespaceList )
316+ if ! ok {
317+ t .Fatalf ("expected *TenantNamespaceList, got %T" , obj )
318+ }
319+ if len (list .Items ) != 1 {
320+ t .Fatalf ("expected 1 item, got %d: %+v" , len (list .Items ), list .Items )
321+ }
322+ if list .Items [0 ].Name != "tenant-allowed" {
323+ t .Errorf ("expected tenant-allowed, got %q" , list .Items [0 ].Name )
324+ }
325+ }
326+
327+ // TestList_PrivilegedGroups_SeeAllTenantNamespaces asserts privileged groups
328+ // get every tenant namespace (and only tenant namespaces) without any
329+ // RoleBindings, sorted by name.
330+ func TestList_PrivilegedGroups_SeeAllTenantNamespaces (t * testing.T ) {
331+ for _ , group := range []string {"system:masters" , "cozystack-cluster-admin" } {
332+ t .Run (group , func (t * testing.T ) {
333+ client := fake .NewClientBuilder ().
334+ WithScheme (newTestScheme (t )).
335+ WithObjects (
336+ & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : "tenant-zebra" }},
337+ & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : "tenant-alpha" }},
338+ & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : "default" }},
339+ ).
340+ Build ()
341+
342+ r := & REST {
343+ c : client ,
344+ gvr : schema.GroupVersionResource {Group : "core.cozystack.io" , Version : "v1alpha1" , Resource : "tenantnamespaces" },
345+ }
346+
347+ u := & user.DefaultInfo {Name : "admin" , Groups : []string {group }}
348+ ctx := request .WithUser (context .Background (), u )
349+
350+ obj , err := r .List (ctx , nil )
351+ if err != nil {
352+ t .Fatalf ("unexpected error: %v" , err )
353+ }
354+ list , ok := obj .(* corev1alpha1.TenantNamespaceList )
355+ if ! ok {
356+ t .Fatalf ("expected *TenantNamespaceList, got %T" , obj )
357+ }
358+ if len (list .Items ) != 2 {
359+ t .Fatalf ("expected 2 items, got %d: %+v" , len (list .Items ), list .Items )
360+ }
361+ for i , want := range []string {"tenant-alpha" , "tenant-zebra" } {
362+ if list .Items [i ].Name != want {
363+ t .Errorf ("item %d: expected %q, got %q" , i , want , list .Items [i ].Name )
364+ }
365+ }
366+ })
367+ }
368+ }
369+
370+ // TestList_MissingUser_ReturnsError asserts List fails when no user is in the
371+ // context instead of returning an unfiltered list.
372+ func TestList_MissingUser_ReturnsError (t * testing.T ) {
373+ client := fake .NewClientBuilder ().
374+ WithScheme (newTestScheme (t )).
375+ WithObjects (& corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : "tenant-test" }}).
376+ Build ()
377+
378+ r := & REST {
379+ c : client ,
380+ gvr : schema.GroupVersionResource {Group : "core.cozystack.io" , Version : "v1alpha1" , Resource : "tenantnamespaces" },
381+ }
382+
383+ obj , err := r .List (context .Background (), nil )
384+ if err == nil {
385+ t .Fatal ("expected error for missing user in context, got nil" )
386+ }
387+ if obj != nil {
388+ t .Errorf ("expected nil list on error, got %+v" , obj )
389+ }
390+ }
391+
392+ // TestList_RoleBindingInNonTenantNamespace_Ignored asserts a RoleBinding
393+ // outside the tenant namespace set grants nothing: filterAccessible
394+ // intersects RoleBinding namespaces with the tenant name-set.
395+ func TestList_RoleBindingInNonTenantNamespace_Ignored (t * testing.T ) {
396+ client := fake .NewClientBuilder ().
397+ WithScheme (newTestScheme (t )).
398+ WithObjects (
399+ & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : "tenant-test" }},
400+ & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : "default" }},
401+ & rbacv1.RoleBinding {
402+ ObjectMeta : metav1.ObjectMeta {Name : "default-binding" , Namespace : "default" },
403+ Subjects : []rbacv1.Subject {{Kind : "User" , Name : "test-user" , APIGroup : "rbac.authorization.k8s.io" }},
404+ RoleRef : rbacv1.RoleRef {APIGroup : "rbac.authorization.k8s.io" , Kind : "Role" , Name : "test-role" },
405+ },
406+ ).
407+ Build ()
408+
409+ r := & REST {
410+ c : client ,
411+ gvr : schema.GroupVersionResource {Group : "core.cozystack.io" , Version : "v1alpha1" , Resource : "tenantnamespaces" },
412+ }
413+
414+ u := & user.DefaultInfo {Name : "test-user" , Groups : []string {"system:authenticated" }}
415+ ctx := request .WithUser (context .Background (), u )
416+
417+ obj , err := r .List (ctx , nil )
418+ if err != nil {
419+ t .Fatalf ("unexpected error: %v" , err )
420+ }
421+ list , ok := obj .(* corev1alpha1.TenantNamespaceList )
422+ if ! ok {
423+ t .Fatalf ("expected *TenantNamespaceList, got %T" , obj )
424+ }
425+ if len (list .Items ) != 0 {
426+ t .Errorf ("expected empty list, got %+v" , list .Items )
427+ }
428+ }
429+
430+ // TestList_NamespaceListError_Propagates asserts a failure to list namespaces
431+ // is returned to the caller.
432+ func TestList_NamespaceListError_Propagates (t * testing.T ) {
433+ wantErr := errors .New ("namespace list failed" )
434+ fc := fake .NewClientBuilder ().
435+ WithScheme (newTestScheme (t )).
436+ WithInterceptorFuncs (interceptor.Funcs {
437+ List : func (ctx context.Context , c client.WithWatch , list client.ObjectList , opts ... client.ListOption ) error {
438+ if _ , ok := list .(* corev1.NamespaceList ); ok {
439+ return wantErr
440+ }
441+ return c .List (ctx , list , opts ... )
442+ },
443+ }).
444+ Build ()
445+
446+ r := & REST {
447+ c : fc ,
448+ gvr : schema.GroupVersionResource {Group : "core.cozystack.io" , Version : "v1alpha1" , Resource : "tenantnamespaces" },
449+ }
450+
451+ u := & user.DefaultInfo {Name : "test-user" , Groups : []string {"system:authenticated" }}
452+ ctx := request .WithUser (context .Background (), u )
453+
454+ if _ , err := r .List (ctx , nil ); ! errors .Is (err , wantErr ) {
455+ t .Fatalf ("expected %v, got %v" , wantErr , err )
456+ }
457+ }
458+
459+ // TestList_RoleBindingListError_Propagates asserts a failure to list
460+ // RoleBindings during access filtering fails the List instead of returning an
461+ // unfiltered or silently truncated result.
462+ func TestList_RoleBindingListError_Propagates (t * testing.T ) {
463+ wantErr := errors .New ("rolebinding list failed" )
464+ fc := fake .NewClientBuilder ().
465+ WithScheme (newTestScheme (t )).
466+ WithObjects (& corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : "tenant-test" }}).
467+ WithInterceptorFuncs (interceptor.Funcs {
468+ List : func (ctx context.Context , c client.WithWatch , list client.ObjectList , opts ... client.ListOption ) error {
469+ if _ , ok := list .(* rbacv1.RoleBindingList ); ok {
470+ return wantErr
471+ }
472+ return c .List (ctx , list , opts ... )
473+ },
474+ }).
475+ Build ()
476+
477+ r := & REST {
478+ c : fc ,
479+ gvr : schema.GroupVersionResource {Group : "core.cozystack.io" , Version : "v1alpha1" , Resource : "tenantnamespaces" },
480+ }
481+
482+ u := & user.DefaultInfo {Name : "test-user" , Groups : []string {"system:authenticated" }}
483+ ctx := request .WithUser (context .Background (), u )
484+
485+ if _ , err := r .List (ctx , nil ); ! errors .Is (err , wantErr ) {
486+ t .Fatalf ("expected %v, got %v" , wantErr , err )
487+ }
488+ }
489+
490+ // TestGet_MissingUser_ReturnsError asserts Get fails when no user is in the
491+ // context.
492+ func TestGet_MissingUser_ReturnsError (t * testing.T ) {
493+ client := fake .NewClientBuilder ().
494+ WithScheme (newTestScheme (t )).
495+ WithObjects (& corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : "tenant-test" }}).
496+ Build ()
497+
498+ r := & REST {
499+ c : client ,
500+ gvr : schema.GroupVersionResource {Group : "core.cozystack.io" , Version : "v1alpha1" , Resource : "tenantnamespaces" },
501+ }
502+
503+ if _ , err := r .Get (context .Background (), "tenant-test" , & metav1.GetOptions {}); err == nil {
504+ t .Fatal ("expected error for missing user in context, got nil" )
505+ }
506+ }
507+
508+ // TestGet_NamespaceNotFound asserts the backing Get error surfaces when the
509+ // namespace disappears after the access check passes.
510+ func TestGet_NamespaceNotFound (t * testing.T ) {
511+ client := fake .NewClientBuilder ().WithScheme (newTestScheme (t )).Build ()
512+
513+ r := & REST {
514+ c : client ,
515+ gvr : schema.GroupVersionResource {Group : "core.cozystack.io" , Version : "v1alpha1" , Resource : "tenantnamespaces" },
516+ }
517+
518+ u := & user.DefaultInfo {Name : "admin" , Groups : []string {"system:masters" }}
519+ ctx := request .WithUser (context .Background (), u )
520+
521+ _ , err := r .Get (ctx , "tenant-missing" , & metav1.GetOptions {})
522+ if err == nil {
523+ t .Fatal ("expected error, got nil" )
524+ }
525+ if ! apierrors .IsNotFound (err ) {
526+ t .Fatalf ("expected NotFound, got %v" , err )
527+ }
528+ }
529+
530+ // TestMatchesSubject_UnknownKind asserts unknown subject kinds never match.
531+ func TestMatchesSubject_UnknownKind (t * testing.T ) {
532+ subj := rbacv1.Subject {Kind : "UnknownKind" , Name : "test-user" }
533+ if matchesSubject (subj , "tenant-test" , "test-user" , map [string ]struct {}{}) {
534+ t .Error ("expected unknown subject kind not to match" )
535+ }
536+ }
537+
538+ func TestHasAccessToNamespace_MissingUser (t * testing.T ) {
539+ client := fake .NewClientBuilder ().
540+ WithScheme (newTestScheme (t )).
541+ Build ()
542+
543+ r := & REST {
544+ c : client ,
545+ gvr : schema.GroupVersionResource {Group : "core.cozystack.io" , Version : "v1alpha1" , Resource : "tenantnamespaces" },
546+ }
547+
548+ hasAccess , err := r .hasAccessToNamespace (context .Background (), "tenant-test" )
549+ if err == nil {
550+ t .Fatal ("expected error for missing user in context, got nil" )
551+ }
552+ if hasAccess {
553+ t .Error ("expected no access when user is missing in context" )
554+ }
555+ }
556+
270557func TestHasAccessToNamespace_ServiceAccount (t * testing.T ) {
271558 scheme := runtime .NewScheme ()
272559 _ = corev1 .AddToScheme (scheme )
0 commit comments