feat(server): add project sharing access control
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
sharingmodule "img_infinite_canvas/internal/modules/sharing"
|
||||
)
|
||||
|
||||
type fakeBearerAuthenticator struct{}
|
||||
@@ -18,6 +19,15 @@ func (fakeBearerAuthenticator) UserIDFromBearer(_ context.Context, authorization
|
||||
return "", false
|
||||
}
|
||||
|
||||
type fakeShareAuthorizer struct {
|
||||
access sharingmodule.Access
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeShareAuthorizer) ResolveAccess(_ context.Context, _ string, _ sharingmodule.Actor) (sharingmodule.Access, error) {
|
||||
return f.access, f.err
|
||||
}
|
||||
|
||||
func TestUserContextMiddlewareUsesAuthorizationHeader(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/projects", nil)
|
||||
req.Header.Set("Authorization", "Bearer valid")
|
||||
@@ -58,3 +68,133 @@ func TestUserContextMiddlewareIgnoresLegacyTokenHeaderAndCookie(t *testing.T) {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserContextMiddlewareIgnoresSpoofedUserIDOnPublicRoute(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/dashboard", nil)
|
||||
req.Header.Set("X-User-Id", "91cd197b-8255-4172-9981-77391fd38f33")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
UserContextMiddleware(fakeBearerAuthenticator{})(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := design.UserIDFromContext(r.Context()); got != design.DefaultUserID {
|
||||
t.Fatalf("expected spoofed identity to be ignored, got %s", got)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNoContent {
|
||||
t.Fatalf("expected public route to continue, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserContextMiddlewareAuthorizesAnonymousCanvasShare(t *testing.T) {
|
||||
ownerID := "d1d2fbbb-97bb-4406-8d0b-ec814cc65092"
|
||||
authorizer := fakeShareAuthorizer{access: sharingmodule.Access{
|
||||
ProjectID: "project-1",
|
||||
OwnerID: ownerID,
|
||||
Permission: sharingmodule.PermissionCanvas,
|
||||
Capabilities: sharingmodule.PermissionCanvas.Capabilities(),
|
||||
}}
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/projects/project-1", nil)
|
||||
req.Header.Set("X-Share-Id", "XhRewKnS4it7X9kEMFLcdVJentg")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
UserContextMiddleware(fakeBearerAuthenticator{}, authorizer)(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := design.UserIDFromContext(r.Context()); got != ownerID {
|
||||
t.Fatalf("expected owner-scoped repository context, got %s", got)
|
||||
}
|
||||
if _, ok := sharingmodule.AccessFromContext(r.Context()); !ok {
|
||||
t.Fatal("expected effective share access in context")
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNoContent {
|
||||
t.Fatalf("expected shared read to continue, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserContextMiddlewareDeniesCanvasShareChatHistory(t *testing.T) {
|
||||
authorizer := fakeShareAuthorizer{access: sharingmodule.Access{
|
||||
ProjectID: "project-1",
|
||||
OwnerID: "d1d2fbbb-97bb-4406-8d0b-ec814cc65092",
|
||||
Permission: sharingmodule.PermissionCanvas,
|
||||
Capabilities: sharingmodule.PermissionCanvas.Capabilities(),
|
||||
}}
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/projects/project-1/history", nil)
|
||||
req.Header.Set("X-Share-Id", "XhRewKnS4it7X9kEMFLcdVJentg")
|
||||
rec := httptest.NewRecorder()
|
||||
called := false
|
||||
|
||||
UserContextMiddleware(fakeBearerAuthenticator{}, authorizer)(func(http.ResponseWriter, *http.Request) {
|
||||
called = true
|
||||
})(rec, req)
|
||||
|
||||
if called {
|
||||
t.Fatal("expected canvas-only chat history to be blocked")
|
||||
}
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected forbidden, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserContextMiddlewareDoesNotLetHeaderOverridePathProject(t *testing.T) {
|
||||
authorizer := fakeShareAuthorizer{access: sharingmodule.Access{
|
||||
ProjectID: "shared-project",
|
||||
OwnerID: "d1d2fbbb-97bb-4406-8d0b-ec814cc65092",
|
||||
Permission: sharingmodule.PermissionEditor,
|
||||
Capabilities: sharingmodule.PermissionEditor.Capabilities(),
|
||||
}}
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/projects/other-project", nil)
|
||||
req.Header.Set("X-Share-Id", "XhRewKnS4it7X9kEMFLcdVJentg")
|
||||
req.Header.Set("X-Share-Project-Id", "shared-project")
|
||||
rec := httptest.NewRecorder()
|
||||
called := false
|
||||
|
||||
UserContextMiddleware(fakeBearerAuthenticator{}, authorizer)(func(http.ResponseWriter, *http.Request) {
|
||||
called = true
|
||||
})(rec, req)
|
||||
|
||||
if called {
|
||||
t.Fatal("expected mismatched path project to be blocked")
|
||||
}
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected not found, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserContextMiddlewareRequiresEditorForGeneratorTasks(t *testing.T) {
|
||||
authorizer := fakeShareAuthorizer{access: sharingmodule.Access{
|
||||
ProjectID: "project-1",
|
||||
OwnerID: "d1d2fbbb-97bb-4406-8d0b-ec814cc65092",
|
||||
Permission: sharingmodule.PermissionViewer,
|
||||
Capabilities: sharingmodule.PermissionViewer.Capabilities(),
|
||||
}}
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/generator/tasks?project_id=project-1&task_id=task-1", nil)
|
||||
req.Header.Set("X-Share-Id", "XhRewKnS4it7X9kEMFLcdVJentg")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
UserContextMiddleware(fakeBearerAuthenticator{}, authorizer)(func(http.ResponseWriter, *http.Request) {
|
||||
t.Fatal("expected viewer generator access to be blocked")
|
||||
})(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected forbidden, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserContextMiddlewareProtectsV1Routes(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/generator/tasks", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
called := false
|
||||
|
||||
UserContextMiddleware(fakeBearerAuthenticator{})(func(http.ResponseWriter, *http.Request) {
|
||||
called = true
|
||||
})(rec, req)
|
||||
|
||||
if called {
|
||||
t.Fatal("expected v1 route to require authentication")
|
||||
}
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected unauthorized, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user