Files
Gardomatic/internal/api/permissions_test.go
T
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

148 lines
6.5 KiB
Go

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)
}
}
}