package api import ( "io" "log/slog" "net/http" "net/http/httptest" "testing" "gardomatic.kleiax.de/internal/storage" ) func TestGardenRolePermissions(t *testing.T) { tests := []struct { role storage.GardenRole permission storage.GardenPermission want int }{ {storage.GardenRoleOwner, storage.GardenPermissionGardenDelete, http.StatusNoContent}, {storage.GardenRoleAdmin, storage.GardenPermissionMembersWrite, http.StatusNoContent}, {storage.GardenRoleAdmin, storage.GardenPermissionGardenDelete, http.StatusForbidden}, {storage.GardenRoleMember, storage.GardenPermissionPlantUpdateOwn, http.StatusNoContent}, {storage.GardenRoleMember, storage.GardenPermissionSpeciesWrite, http.StatusForbidden}, {storage.GardenRoleViewer, storage.GardenPermissionPlantUpdateOwn, http.StatusForbidden}, {storage.GardenRoleWorker, storage.GardenPermissionTaskCompleteOther, http.StatusNoContent}, {storage.GardenRoleWorker, storage.GardenPermissionTaskUpdateOther, http.StatusForbidden}, } app := &application{logger: slog.New(slog.NewTextHandler(io.Discard, nil))} for _, tt := range tests { t.Run(string(tt.role)+"/"+string(tt.permission), func(t *testing.T) { next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) }) handler := app.requireGardenPermission(tt.permission, next) request := httptest.NewRequest(http.MethodPost, "/", nil) request = app.contextSetGardenMember(request, storage.GardenMember{Role: tt.role}) response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != tt.want { t.Fatalf("status = %d, want %d", response.Code, tt.want) } }) } } func TestPersistedGardenAdminWildcardDoesNotGrantOwnershipRights(t *testing.T) { member := storage.GardenMember{Role: storage.GardenRoleAdmin, Permissions: []storage.GardenPermission{"garden:*"}} if member.Can(storage.GardenPermissionGardenDelete) { t.Fatal("garden:* must not grant the owner-only garden:delete permission") } if !member.Can(storage.GardenPermissionMembersWrite) { t.Fatal("garden:* must grant ordinary garden administration permissions") } } func TestGardenPermissionOverridesResolveWildcards(t *testing.T) { permissions := storage.ResolveGardenPermissions([]string{"garden:*"}, []storage.GardenRolePermissionOverride{{Permission: string(storage.GardenPermissionMembersWrite), Granted: false}, {Permission: string(storage.GardenPermissionGardenDelete), Granted: true}}) member := storage.GardenMember{Permissions: permissions} if member.Can(storage.GardenPermissionMembersWrite) || !member.Can(storage.GardenPermissionGardenDelete) { t.Fatalf("unexpected effective permissions: %v", permissions) } } func TestGardenRoleBundlesConcretePermissions(t *testing.T) { member := storage.GardenRoleMember.Permissions() found := false for _, permission := range member { if permission == storage.GardenPermissionPlantUpdateOther { found = true } } if found { t.Fatal("member role must not bundle permissions for other users' plants") } if len(storage.GardenRoleAdmin.Permissions()) <= len(member) { t.Fatal("admin role must bundle more permissions than member") } } func TestGardenResourcePermissionsDistinguishOwnAndOtherObjects(t *testing.T) { app := &application{logger: slog.New(slog.NewTextHandler(io.Discard, nil))} tests := []struct { name string role storage.GardenRole createdBy int want int }{ {"member may update own plant", storage.GardenRoleMember, 7, http.StatusNoContent}, {"member may not update another plant", storage.GardenRoleMember, 8, http.StatusForbidden}, {"admin may update another plant", storage.GardenRoleAdmin, 8, http.StatusNoContent}, {"viewer may read another task", storage.GardenRoleViewer, 8, http.StatusNoContent}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { request := httptest.NewRequest(http.MethodPatch, "/", nil) request = app.contextSetAuthenticatedUser(request, storage.User{ID: 7}) request = app.contextSetGardenMember(request, storage.GardenMember{Role: tt.role}) response := httptest.NewRecorder() own, other := storage.GardenPermissionPlantUpdateOwn, storage.GardenPermissionPlantUpdateOther if tt.role == storage.GardenRoleViewer { own, other = storage.GardenPermissionTaskReadOwn, storage.GardenPermissionTaskReadOther } if app.authorizeGardenResource(response, request, tt.createdBy, own, other) { response.WriteHeader(http.StatusNoContent) } if response.Code != tt.want { t.Fatalf("status = %d, want %d", response.Code, tt.want) } }) } } func TestApplicationRolePermissions(t *testing.T) { if !storage.ApplicationRoleAdmin.Can(storage.ApplicationPermissionGlobalSpeciesWrite) { t.Fatal("admin role must grant global species write permission") } if storage.ApplicationRoleUser.Can(storage.ApplicationPermissionGlobalSpeciesWrite) { t.Fatal("user role must not grant global species write permission") } if !storage.ApplicationRoleUser.Can(storage.ApplicationPermissionGardensCreate) || !storage.ApplicationRoleAdmin.Can(storage.ApplicationPermissionGardensCreate) { t.Fatal("built-in application roles must retain permission to create gardens") } if !storage.ApplicationRoleAdmin.Valid() || !storage.ApplicationRoleUser.Valid() || storage.ApplicationRole("unknown").Valid() { t.Fatal("application role validation returned an unexpected result") } } func TestRequireApplicationPermission(t *testing.T) { app := &application{logger: slog.New(slog.NewTextHandler(io.Discard, nil))} next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) }) handler := app.requireApplicationPermission(storage.ApplicationPermissionUsersManage, next) for _, test := range []struct { name string user storage.User want int }{ {"admin role", storage.User{Role: storage.ApplicationRoleAdmin, Permissions: storage.ApplicationRoleAdmin.Permissions()}, http.StatusNoContent}, {"user role", storage.User{Role: storage.ApplicationRoleUser, Permissions: storage.ApplicationRoleUser.Permissions()}, http.StatusForbidden}, {"custom server role", storage.User{Role: "application:user-manager", Permissions: []storage.ApplicationPermission{storage.ApplicationPermissionUsersManage}}, http.StatusNoContent}, } { request := httptest.NewRequest(http.MethodGet, "/v1/admin/users", nil) request = app.contextSetAuthenticatedUser(request, test.user) response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != test.want { t.Errorf("%s: got %d, want %d", test.name, response.Code, test.want) } } }