@@ -0,0 +1,147 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user