Skip to content

Commit da4ae1a

Browse files
committed
Fix staging impersonation and integration timeouts
1 parent 6de8a8f commit da4ae1a

File tree

17 files changed

+220
-56
lines changed

17 files changed

+220
-56
lines changed

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
1818

1919
# Set to "true" to enable inference UI in CLI and web.
2020
INFER_ENABLED ?= false
21+
INTEGRATION_TIMEOUT ?= 45m
2122

2223
LDFLAGS := -X main.version=$(VERSION) \
2324
-X main.commit=$(COMMIT) \
@@ -78,11 +79,11 @@ test:
7879

7980
## test-integration: run all tests including integration
8081
test-integration:
81-
go test -tags integration -timeout 20m ./...
82+
go test -tags integration -timeout $(INTEGRATION_TIMEOUT) ./...
8283

8384
## test-integration-offline: run offline integration tests only (no network, requires dolt)
8485
test-integration-offline:
85-
go test -tags integration -v -timeout 20m ./internal/remote/ ./test/integration/offline/
86+
go test -tags integration -v -timeout $(INTEGRATION_TIMEOUT) ./internal/remote/ ./test/integration/offline/
8687

8788
## test-cover: run tests with coverage output
8889
test-cover:

cmd/wl/cmd_accept.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ The item must be in 'in_review' status.
2929
A stamp is created with quality and optional reliability ratings (1-5),
3030
severity (leaf/branch/root), and optional skill tags.
3131
32+
Self-stamps are not allowed. If you completed the item yourself, use
33+
'wl close' instead.
34+
3235
In wild-west mode the commit is auto-pushed to upstream and origin.
3336
Use --no-push to skip pushing (offline work).
3437
@@ -114,7 +117,11 @@ func runAccept(cmd *cobra.Command, stdout, _ io.Writer, wantedID string, quality
114117
}
115118

116119
renderMutationResult(stdout, "Accepted", wantedID, result, extras...)
117-
printNextHint(stdout, "Next: stamp issued. View: wl status "+wantedID)
120+
nextHint := "Next: stamp issued. View: wl status " + wantedID
121+
if result.Detail == nil || result.Detail.Stamp == nil {
122+
nextHint = "Next: item completed without a stamp. View: wl status " + wantedID
123+
}
124+
printNextHint(stdout, nextHint)
118125

119126
return nil
120127
}

cmd/wl/cmd_accept_upstream.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ This adopts the submitter's upstream PR into the main wanted item, creates a
2828
completion record, and issues a stamp. The submitter must currently have an
2929
in-review upstream submission for the wanted item.
3030
31+
Self-stamps are not allowed. If the submitter is you, use 'wl close-upstream'
32+
instead.
33+
3134
Examples:
3235
wl accept-upstream w-abc123 charlie --quality 4
3336
wl accept-upstream w-abc123 charlie --quality 5 --reliability 4 --severity branch

internal/commons/lifecycle.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func CanPerformTransition(item *WantedItem, t Transition, actor string) bool {
157157
case TransitionDone:
158158
return item.ClaimedBy == actor
159159
case TransitionAccept:
160-
return isPoster || isAdmin
160+
return (isPoster || isAdmin) && item.ClaimedBy != actor
161161
case TransitionReject:
162162
return isPoster || isAdmin
163163
case TransitionClose:

internal/commons/lifecycle_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ func TestCanPerformTransition_Accept(t *testing.T) {
122122
PostedBy: "poster",
123123
ClaimedBy: "poster",
124124
}
125-
if !CanPerformTransition(selfItem, TransitionAccept, "poster") {
126-
t.Error("poster should be able to accept own claimed work")
125+
if CanPerformTransition(selfItem, TransitionAccept, "poster") {
126+
t.Error("poster should not be able to accept own claimed work")
127127
}
128128
if CanPerformTransition(item, TransitionAccept, "claimer") {
129129
t.Error("claimer should not be able to accept without poster/admin rights")
@@ -161,8 +161,8 @@ func TestCanPerformTransition_Admin(t *testing.T) {
161161
PostedBy: "poster",
162162
ClaimedBy: "csells",
163163
}
164-
if !CanPerformTransition(selfItem, TransitionAccept, "csells") {
165-
t.Error("admin who claimed should be able to accept")
164+
if CanPerformTransition(selfItem, TransitionAccept, "csells") {
165+
t.Error("admin who claimed should not be able to accept")
166166
}
167167
// Admin cannot delete (poster-only).
168168
openItem := &WantedItem{

internal/hosted/auth.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,9 @@ func (s *Server) AuthMiddleware(next http.Handler) http.Handler {
143143
s.sessions.RememberActiveUpstream(sessionID, upstream)
144144
r.Header.Set("X-Wasteland", upstream)
145145

146-
// Staging-only impersonation: X-Impersonate header overrides rig handle
147-
// for read-only requests so operators can see the UI as another user.
146+
// Staging-only impersonation: X-Impersonate overrides the rig handle so
147+
// operators can exercise the UI and backend flows as another user.
148148
if impersonate := r.Header.Get("X-Impersonate"); impersonate != "" && s.environment == "staging" {
149-
if r.Method != http.MethodGet {
150-
writeJSON(w, http.StatusForbidden, map[string]string{"error": "impersonation is read-only"})
151-
return
152-
}
153149
slog.Info("staging impersonation active", "real", client.RigHandle(), "impersonate", impersonate)
154150
client = client.WithRigHandle(impersonate)
155151
}

internal/hosted/auth_test.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -997,7 +997,7 @@ func TestAuthMiddleware_Impersonate_Staging_PreservesResolvedViewerIdentity(t *t
997997
}
998998
}
999999

1000-
func TestAuthMiddleware_Impersonate_Staging_POST_Blocked(t *testing.T) {
1000+
func TestAuthMiddleware_Impersonate_Staging_POST_Allowed(t *testing.T) {
10011001
sessions, ts := setupStagingTestServer(t)
10021002
sessionID, _ := sessions.Create("conn-1")
10031003

@@ -1012,9 +1012,18 @@ func TestAuthMiddleware_Impersonate_Staging_POST_Blocked(t *testing.T) {
10121012
}
10131013
defer resp.Body.Close() //nolint:errcheck // test cleanup
10141014

1015-
if resp.StatusCode != http.StatusForbidden {
1015+
if resp.StatusCode != http.StatusOK {
10161016
body, _ := io.ReadAll(resp.Body)
1017-
t.Errorf("expected 403, got %d: %s", resp.StatusCode, string(body))
1017+
t.Fatalf("expected 200, got %d: %s", resp.StatusCode, string(body))
1018+
}
1019+
1020+
var result map[string]string
1021+
_ = json.NewDecoder(resp.Body).Decode(&result)
1022+
if result["rig_handle"] != "bob" {
1023+
t.Errorf("expected impersonated rig_handle=bob, got %s", result["rig_handle"])
1024+
}
1025+
if result["viewer"] != "alice" {
1026+
t.Errorf("expected resolved viewer=alice, got %s", result["viewer"])
10181027
}
10191028
}
10201029

internal/hosted/authservice_integration_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,91 @@ func (f *fakeAuthService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
138138
http.Error(w, "not found", http.StatusNotFound)
139139
}
140140

141+
func setupAuthServiceStagingMiddlewareTestServer(t *testing.T) (*SessionStore, *httptest.Server) {
142+
t.Helper()
143+
144+
authStub := &fakeAuthService{t: t}
145+
authTS := httptest.NewServer(authStub)
146+
t.Cleanup(authTS.Close)
147+
148+
authClient := dolthubauth.NewClient(dolthubauth.ClientConfig{
149+
BaseURL: authTS.URL,
150+
TenantID: "tenant-1",
151+
Environment: "staging",
152+
KeyID: "kid-1",
153+
SharedSecret: "shared-secret",
154+
Now: func() time.Time {
155+
return time.Date(2026, 4, 8, 12, 0, 0, 0, time.UTC)
156+
},
157+
})
158+
159+
sessions := NewSessionStore()
160+
resolver := NewAuthServiceWorkspaceResolver(authClient, sessions)
161+
t.Cleanup(resolver.Stop)
162+
163+
hostedServer := NewAuthServiceServer(resolver, sessions, authClient, "session-secret", "subject-secret", "staging")
164+
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
165+
client, ok := ClientFromContext(r.Context())
166+
if !ok {
167+
w.WriteHeader(http.StatusInternalServerError)
168+
return
169+
}
170+
scope, _ := api.ResolvedReadIdentityFromContext(r.Context())
171+
writeJSON(w, http.StatusOK, map[string]string{
172+
"rig_handle": client.RigHandle(),
173+
"upstream": scope.Upstream,
174+
"viewer": scope.Viewer,
175+
})
176+
})
177+
178+
mux := http.NewServeMux()
179+
mux.Handle("/", hostedServer.AuthMiddleware(inner))
180+
ts := httptest.NewServer(mux)
181+
t.Cleanup(ts.Close)
182+
return sessions, ts
183+
}
184+
185+
func TestAuthServiceAuthMiddleware_Impersonate_Staging_POST_Allowed(t *testing.T) {
186+
sessions, ts := setupAuthServiceStagingMiddlewareTestServer(t)
187+
sessionID, err := sessions.CreateWithSubject("conn-1", "subject-1")
188+
if err != nil {
189+
t.Fatalf("CreateWithSubject() error = %v", err)
190+
}
191+
192+
req, err := http.NewRequest(http.MethodPost, ts.URL+"/api/wanted", nil)
193+
if err != nil {
194+
t.Fatalf("new request: %v", err)
195+
}
196+
req.AddCookie(&http.Cookie{Name: cookieName, Value: SignSessionCookie(sessionID, "conn-1", "session-secret")})
197+
req.AddCookie(&http.Cookie{Name: subjectCookieName, Value: SignSubjectID("subject-1", "subject-secret")})
198+
req.Header.Set("X-Wasteland", "hop/wl-commons")
199+
req.Header.Set("X-Impersonate", "bob")
200+
201+
resp, err := http.DefaultClient.Do(req)
202+
if err != nil {
203+
t.Fatalf("POST /api/wanted: %v", err)
204+
}
205+
defer func() { _ = resp.Body.Close() }()
206+
if resp.StatusCode != http.StatusOK {
207+
body, _ := io.ReadAll(resp.Body)
208+
t.Fatalf("expected 200, got %d: %s", resp.StatusCode, string(body))
209+
}
210+
211+
var result map[string]string
212+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
213+
t.Fatalf("decode response: %v", err)
214+
}
215+
if result["rig_handle"] != "bob" {
216+
t.Fatalf("rig_handle = %q, want bob", result["rig_handle"])
217+
}
218+
if result["viewer"] != "alice" {
219+
t.Fatalf("viewer = %q, want alice", result["viewer"])
220+
}
221+
if result["upstream"] == "" {
222+
t.Fatal("expected resolved upstream to be set")
223+
}
224+
}
225+
141226
func TestAuthServiceHostedEndToEnd(t *testing.T) {
142227
authStub := &fakeAuthService{t: t}
143228
authTS := httptest.NewServer(authStub)

internal/hosted/authservice_server.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -547,10 +547,6 @@ func (s *AuthServiceServer) AuthMiddleware(next http.Handler) http.Handler {
547547
r.Header.Set("X-Wasteland", upstream)
548548

549549
if impersonate := r.Header.Get("X-Impersonate"); impersonate != "" && s.environment == "staging" {
550-
if r.Method != http.MethodGet {
551-
writeJSON(w, http.StatusForbidden, map[string]string{"error": "impersonation is read-only"})
552-
return
553-
}
554550
client = client.WithRigHandle(impersonate)
555551
}
556552

internal/sdk/additional_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,42 @@ func TestCloseUpstream(t *testing.T) {
851851
}
852852
}
853853

854+
func TestCloseUpstream_SelfAllowed(t *testing.T) {
855+
db := newFakeDB()
856+
db.seedItem(fakeItem{ID: "w-1", Title: "Fix bug", Status: "in_review", PostedBy: "alice", EffortLevel: "medium"})
857+
858+
c := New(ClientConfig{
859+
DB: db,
860+
RigHandle: "charlie",
861+
Mode: "wild-west",
862+
ListPendingItems: pendingItems(map[string][]PendingItem{
863+
"w-1": {{
864+
RigHandle: "charlie",
865+
Status: "in_review",
866+
CompletedBy: "charlie",
867+
Evidence: "proof",
868+
}},
869+
}),
870+
})
871+
872+
result, err := c.CloseUpstream("w-1", "charlie")
873+
if err != nil {
874+
t.Fatalf("CloseUpstream() error = %v", err)
875+
}
876+
if result.Detail == nil || result.Detail.Item == nil {
877+
t.Fatal("CloseUpstream() should return detail with item")
878+
}
879+
if result.Detail.Item.Status != "completed" {
880+
t.Fatalf("status = %q, want completed", result.Detail.Item.Status)
881+
}
882+
if db.completions["w-1"].CompletedBy != "charlie" {
883+
t.Fatalf("completion = %q, want charlie", db.completions["w-1"].CompletedBy)
884+
}
885+
if db.completions["w-1"].StampID != "" {
886+
t.Fatalf("stamp_id = %q, want empty", db.completions["w-1"].StampID)
887+
}
888+
}
889+
854890
func TestFindUpstreamSubmissionErrorsAndLeaderboard(t *testing.T) {
855891
t.Run("find upstream submission errors", func(t *testing.T) {
856892
c := New(ClientConfig{DB: newFakeDB(), RigHandle: "alice"})

0 commit comments

Comments
 (0)