Files

358 lines
13 KiB
Go
Raw Permalink Normal View History

package middleware
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/Silo-Server/silo-server/internal/access"
"github.com/Silo-Server/silo-server/internal/auth"
"github.com/Silo-Server/silo-server/internal/models"
"github.com/Silo-Server/silo-server/internal/policy"
"github.com/go-chi/chi/v5"
)
func TestPolicyActingAdminMiddlewareParity(t *testing.T) {
pdp := newMiddlewarePolicyPDP(t)
checkErr := errors.New("profile store down")
tests := []struct {
name string
claims *auth.Claims
profileID string
check PrimaryProfileChecker
}{
{name: "missing_claims"},
{name: "non_admin", claims: &auth.Claims{UserID: 7, Role: "user", TokenType: auth.TokenTypeAccess}},
{name: "admin_without_profile", claims: &auth.Claims{UserID: 7, Role: "admin", TokenType: auth.TokenTypeAccess}, check: primaryChecker(false, true, nil)},
{name: "admin_primary_profile", claims: &auth.Claims{UserID: 7, Role: "admin", TokenType: auth.TokenTypeAccess}, profileID: "prof-1", check: primaryChecker(true, true, nil)},
{name: "admin_non_primary_profile", claims: &auth.Claims{UserID: 7, Role: "admin", TokenType: auth.TokenTypeAccess}, profileID: "prof-2", check: primaryChecker(false, true, nil)},
{name: "admin_unknown_profile", claims: &auth.Claims{UserID: 7, Role: "admin", TokenType: auth.TokenTypeAccess}, profileID: "prof-x", check: primaryChecker(false, false, nil)},
{name: "checker_error", claims: &auth.Claims{UserID: 7, Role: "admin", TokenType: auth.TokenTypeAccess}, profileID: "prof-1", check: primaryChecker(false, false, checkErr)},
{name: "nil_checker_allows_declared_profile", claims: &auth.Claims{UserID: 7, Role: "admin", TokenType: auth.TokenTypeAccess}, profileID: "prof-2"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
legacy := captureActingAdminResponse(RequireActingAdmin(test.check), test.claims, test.profileID)
policyBacked := captureActingAdminResponse(NewPolicyActingAdminMiddleware(pdp, test.check), test.claims, test.profileID)
assertMiddlewareResponsesEqual(t, policyBacked, legacy)
})
}
}
func TestPolicyMetadataCurationMiddlewareParity(t *testing.T) {
pdp := newMiddlewarePolicyPDP(t)
resolverErr := errors.New("resolver down")
userErr := errors.New("user store down")
curator := &models.User{ID: 7, Role: "user", Enabled: true, LibraryIDs: []int{1, 2, 3}, Permissions: []string{policy.PermissionMetadataCuration}}
unrestrictedCurator := &models.User{ID: 7, Role: "user", Enabled: true, LibraryIDs: nil, Permissions: []string{policy.PermissionMetadataCuration}}
noPermission := &models.User{ID: 7, Role: "user", Enabled: true, LibraryIDs: []int{1}, Permissions: nil}
disabledCurator := &models.User{ID: 7, Role: "user", Enabled: false, LibraryIDs: nil, Permissions: []string{policy.PermissionMetadataCuration}}
nonPrimaryAdmin := &models.User{ID: 7, Role: "admin", Enabled: true, LibraryIDs: nil, Permissions: nil}
assignedNonPrimaryAdmin := &models.User{ID: 7, Role: "admin", Enabled: true, LibraryIDs: nil, Permissions: []string{policy.PermissionMetadataCuration}}
tests := []struct {
name string
claims *auth.Claims
user *models.User
userErr error
targetIDs []int
targetErr error
profileID string
check PrimaryProfileChecker
itemID string
}{
{name: "missing_claims", itemID: "item-1"},
{name: "acting_admin_primary_bypasses_missing_repos", claims: adminClaims(), profileID: "prof-1", check: primaryChecker(true, true, nil), itemID: "item-1"},
{name: "acting_admin_no_profile_bypasses", claims: adminClaims(), itemID: "item-1"},
{name: "non_primary_admin_without_assigned_permission", claims: adminClaims(), user: nonPrimaryAdmin, targetIDs: []int{1}, profileID: "prof-2", check: primaryChecker(false, true, nil), itemID: "item-1"},
{name: "non_primary_admin_with_assigned_permission", claims: adminClaims(), user: assignedNonPrimaryAdmin, targetIDs: []int{1}, profileID: "prof-2", check: primaryChecker(false, true, nil), itemID: "item-1"},
{name: "non_primary_admin_with_assigned_permission_out_of_scope", claims: adminClaims(), user: &models.User{ID: 7, Role: "admin", Enabled: true, LibraryIDs: []int{1}, Permissions: []string{policy.PermissionMetadataCuration}}, targetIDs: []int{2}, profileID: "prof-2", check: primaryChecker(false, true, nil), itemID: "item-1"},
{name: "user_without_permission", claims: userClaims(), user: noPermission, targetIDs: []int{1}, itemID: "item-1"},
{name: "unrestricted_curator", claims: userClaims(), user: unrestrictedCurator, targetIDs: []int{8, 9}, itemID: "item-1"},
{name: "curator_in_scope", claims: userClaims(), user: curator, targetIDs: []int{1, 3}, itemID: "item-1"},
{name: "curator_out_of_scope", claims: userClaims(), user: curator, targetIDs: []int{1, 4}, itemID: "item-1"},
{name: "target_resolver_error", claims: userClaims(), user: curator, targetErr: resolverErr, itemID: "item-1"},
{name: "target_not_found", claims: userClaims(), user: unrestrictedCurator, targetIDs: nil, itemID: "item-1"},
{name: "missing_item_id", claims: userClaims(), user: curator},
{name: "user_loader_error", claims: userClaims(), userErr: userErr, itemID: "item-1"},
{name: "user_not_found", claims: userClaims(), user: nil, itemID: "item-1"},
{name: "user_disabled", claims: userClaims(), user: disabledCurator, itemID: "item-1"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
legacy := captureMetadataCurationResponse(
NewPermissionMiddleware(
fakePermissionUserLoader{user: test.user, err: test.userErr},
fakeTargetLibraryResolver{ids: test.targetIDs, err: test.targetErr},
test.check,
),
test.claims,
test.profileID,
test.itemID,
)
policyBacked := captureMetadataCurationResponse(
NewPolicyPermissionMiddleware(
fakePermissionUserLoader{user: test.user, err: test.userErr},
fakeTargetLibraryResolver{ids: test.targetIDs, err: test.targetErr},
test.check,
pdp,
),
test.claims,
test.profileID,
test.itemID,
)
assertMiddlewareResponsesEqual(t, policyBacked, legacy)
})
}
}
func TestPolicyActingAdminMiddlewareEvalErrorIsInternal(t *testing.T) {
rec := captureActingAdminResponse(
NewPolicyActingAdminMiddleware(errorPermissionDecider{}, nil),
adminClaims(),
"",
)
if rec.code != http.StatusInternalServerError {
t.Fatalf("status = %d, want %d; body %s", rec.code, http.StatusInternalServerError, rec.body)
}
}
func TestPolicyMetadataCurationMiddlewareAppliesGroupPermissionMask(t *testing.T) {
user := &models.User{
ID: 7,
Role: "user",
Enabled: true,
LibraryIDs: []int{1},
Permissions: []string{policy.PermissionMetadataCuration},
}
rec := captureMetadataCurationResponse(
NewPolicyPermissionMiddleware(
fakePermissionUserLoader{user: user},
fakeTargetLibraryResolver{ids: []int{1}},
nil,
newMiddlewarePolicyPDP(t),
middlewareGroupProvider{group: &access.GroupPolicy{
AllowedPermissions: []string{policy.PermissionMarkerEdit},
DownloadAllowed: true,
DownloadTranscodeAllowed: true,
RequestsAllowed: true,
}},
),
userClaims(),
"",
"item-1",
)
if rec.code != http.StatusForbidden {
t.Fatalf("status = %d body = %s, want forbidden", rec.code, rec.body)
}
}
func TestPolicyMarkerEditMiddlewareParity(t *testing.T) {
pdp := newMiddlewarePolicyPDP(t)
userErr := errors.New("user store down")
editor := &models.User{ID: 7, Role: "user", Enabled: true, Permissions: []string{policy.PermissionMarkerEdit}}
noPermission := &models.User{ID: 7, Role: "user", Enabled: true, Permissions: nil}
disabledEditor := &models.User{ID: 7, Role: "user", Enabled: false, Permissions: []string{policy.PermissionMarkerEdit}}
enabledAdmin := &models.User{ID: 7, Role: "admin", Enabled: true, Permissions: nil}
tests := []struct {
name string
claims *auth.Claims
user *models.User
userErr error
}{
{name: "missing_claims"},
{name: "admin", claims: adminClaims(), user: enabledAdmin},
{name: "user_with_permission", claims: userClaims(), user: editor},
{name: "user_without_permission", claims: userClaims(), user: noPermission},
{name: "user_disabled", claims: userClaims(), user: disabledEditor},
{name: "user_loader_error", claims: userClaims(), userErr: userErr},
{name: "user_not_found", claims: userClaims(), user: nil},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
legacy := captureMarkerEditResponse(
NewPermissionMiddleware(
fakePermissionUserLoader{user: test.user, err: test.userErr},
nil,
nil,
),
test.claims,
)
policyBacked := captureMarkerEditResponse(
NewPolicyPermissionMiddleware(
fakePermissionUserLoader{user: test.user, err: test.userErr},
nil,
nil,
pdp,
),
test.claims,
)
assertMiddlewareResponsesEqual(t, policyBacked, legacy)
})
}
}
func TestPolicyMarkerEditMiddlewareAppliesGroupPermissionMask(t *testing.T) {
user := &models.User{
ID: 7,
Role: "user",
Enabled: true,
Permissions: []string{policy.PermissionMarkerEdit},
}
rec := captureMarkerEditResponse(
NewPolicyPermissionMiddleware(
fakePermissionUserLoader{user: user},
nil,
nil,
newMiddlewarePolicyPDP(t),
middlewareGroupProvider{group: &access.GroupPolicy{
AllowedPermissions: []string{policy.PermissionMetadataCuration},
DownloadAllowed: true,
DownloadTranscodeAllowed: true,
RequestsAllowed: true,
}},
),
userClaims(),
)
if rec.code != http.StatusForbidden {
t.Fatalf("status = %d body = %s, want forbidden", rec.code, rec.body)
}
}
func TestPolicyMarkerEditMiddlewareEvalErrorIsInternal(t *testing.T) {
user := &models.User{ID: 7, Role: "user", Enabled: true, Permissions: []string{policy.PermissionMarkerEdit}}
rec := captureMarkerEditResponse(
NewPolicyPermissionMiddleware(
fakePermissionUserLoader{user: user},
nil,
nil,
errorPermissionDecider{},
),
userClaims(),
)
if rec.code != http.StatusInternalServerError {
t.Fatalf("status = %d, want %d; body %s", rec.code, http.StatusInternalServerError, rec.body)
}
}
func newMiddlewarePolicyPDP(t *testing.T) *policy.PDP {
t.Helper()
engine, err := policy.NewEngine(context.Background())
if err != nil {
t.Fatalf("NewEngine() error: %v", err)
}
return policy.NewPDP(engine)
}
type middlewareResponse struct {
code int
body string
}
func captureActingAdminResponse(
mw func(http.Handler) http.Handler,
claims *auth.Claims,
profileID string,
) middlewareResponse {
next := mw(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodGet, "/admin/sessions", nil)
if profileID != "" {
req.Header.Set("X-Profile-Id", profileID)
}
if claims != nil {
req = req.WithContext(SetClaims(req.Context(), claims))
}
rec := httptest.NewRecorder()
next.ServeHTTP(rec, req)
return middlewareResponse{code: rec.Code, body: rec.Body.String()}
}
type metadataCurationGate interface {
RequireMetadataCurationForItem(http.Handler) http.Handler
}
func captureMetadataCurationResponse(
mw metadataCurationGate,
claims *auth.Claims,
profileID string,
itemID string,
) middlewareResponse {
next := mw.RequireMetadataCurationForItem(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodPost, "/admin/items/"+itemID+"/refresh-metadata", nil)
if profileID != "" {
req.Header.Set("X-Profile-Id", profileID)
}
ctx := req.Context()
if claims != nil {
ctx = SetClaims(ctx, claims)
}
routeCtx := chi.NewRouteContext()
if itemID != "" {
routeCtx.URLParams.Add("id", itemID)
}
ctx = context.WithValue(ctx, chi.RouteCtxKey, routeCtx)
rec := httptest.NewRecorder()
next.ServeHTTP(rec, req.WithContext(ctx))
return middlewareResponse{code: rec.Code, body: rec.Body.String()}
}
type markerEditGate interface {
RequireMarkerEdit(http.Handler) http.Handler
}
func captureMarkerEditResponse(mw markerEditGate, claims *auth.Claims) middlewareResponse {
next := mw.RequireMarkerEdit(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodPut, "/markers/files/5", nil)
if claims != nil {
req = req.WithContext(SetClaims(req.Context(), claims))
}
rec := httptest.NewRecorder()
next.ServeHTTP(rec, req)
return middlewareResponse{code: rec.Code, body: rec.Body.String()}
}
func assertMiddlewareResponsesEqual(t *testing.T, got, want middlewareResponse) {
t.Helper()
if got != want {
t.Fatalf("policy response = %#v, want legacy %#v", got, want)
}
}
func adminClaims() *auth.Claims {
return &auth.Claims{UserID: 7, Role: "admin", TokenType: auth.TokenTypeAccess}
}
func userClaims() *auth.Claims {
return &auth.Claims{UserID: 7, Role: "user", TokenType: auth.TokenTypeAccess}
}
type errorPermissionDecider struct{}
func (errorPermissionDecider) CheckPermission(context.Context, policy.PermissionInput) (policy.PermissionDecision, policy.Meta, error) {
return policy.PermissionDecision{}, policy.Meta{}, errors.New("policy unavailable")
}
type middlewareGroupProvider struct {
group *access.GroupPolicy
err error
}
func (p middlewareGroupProvider) GetPolicyForUser(context.Context, int) (*access.GroupPolicy, error) {
return p.group, p.err
}