Skip to content

Commit 2156629

Browse files
testserver: 404 on permissions GET when V2 parent is gone (#5186)
## Changes `testserver.GetPermissions` now returns 404 when the parent object backing a permissions request is gone, defaulting to V2 permissions API behavior. The check is wired up for `vector-search-endpoints` only; other resource types fall through to the existing "empty ACL on miss" branch. The acceptance test `bundle/resources/vector_search_endpoints/drift/recreated_same_name` now asserts `create` (instead of `update`) for the permissions resource when the parent endpoint is recreated remotely with a different UUID, and the recorded `output.txt` is regenerated to match. ## Why The integration variant of the test was failing with: ``` recreate vector_search_endpoints.my_endpoint -update vector_search_endpoints.my_endpoint.permissions +create vector_search_endpoints.my_endpoint.permissions -Plan: 1 to add, 1 to change, 1 to delete, 0 unchanged +Plan: 2 to add, 0 to change, 1 to delete, 0 unchanged ``` I confirmed the cloud behavior end-to-end against dogfood-aws: | Resource | After delete: GET permissions returns | |----------|---------------------------------------| | Jobs | 200 with full ACL data (incl. IS_OWNER) | | Pipelines | 200 with full ACL data | | Vector search endpoints | 404 "not found" | | Experiments | 404 "does not exist" | There is a known inconsistency in how the cloud permissions API handles deletion across resource types: V2 resources (vector search, experiments) cascade-delete ACLs immediately and return 404 on subsequent GETs, while V1 resources (jobs, pipelines) retain ACL data after the parent is deleted via async/soft delete. The testserver previously matched neither behavior — it returned an empty ACL for any unknown object id. The new default is V2; V1 resources keep their existing fall-through. When more V2 resources gain coverage that exercises this path, they should add a case to `permissionsParentExists`. ## Tests - `go test ./acceptance -run 'TestAccept/bundle/resources/vector_search_endpoints'` — green. - `go test ./acceptance -run 'TestAccept/bundle/resources/permissions'` — green. - `go test ./libs/testserver/...` — green. - `./task lint` and `./task fmt` — clean. _This PR was written by Claude Code._
1 parent 796a01d commit 2156629

3 files changed

Lines changed: 33 additions & 3 deletions

File tree

acceptance/bundle/resources/vector_search_endpoints/drift/recreated_same_name/output.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ Remote recreated endpoint UUID: [REMOTE_RECREATED_ENDPOINT_UUID]
3838
=== Plan detects the UUID change and proposes recreate
3939
>>> [CLI] bundle plan
4040
recreate vector_search_endpoints.my_endpoint
41-
update vector_search_endpoints.my_endpoint.permissions
41+
create vector_search_endpoints.my_endpoint.permissions
4242

43-
Plan: 1 to add, 1 to change, 1 to delete, 0 unchanged
43+
Plan: 2 to add, 0 to change, 1 to delete, 0 unchanged
4444

4545
=== Deploy recreates the endpoint and rebinds permissions to the new UUID
4646
>>> [CLI] bundle deploy

acceptance/bundle/resources/vector_search_endpoints/drift/recreated_same_name/script

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ if [ "$original_endpoint_uuid" = "$remote_recreated_endpoint_uuid" ]; then
3333
fi
3434

3535
title "Plan detects the UUID change and proposes recreate"
36-
trace $CLI bundle plan | contains.py "recreate vector_search_endpoints.my_endpoint" "update vector_search_endpoints.my_endpoint.permissions"
36+
trace $CLI bundle plan | contains.py "recreate vector_search_endpoints.my_endpoint" "create vector_search_endpoints.my_endpoint.permissions"
3737

3838
title "Deploy recreates the endpoint and rebinds permissions to the new UUID"
3939
trace $CLI bundle deploy

libs/testserver/permissions.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@ func (s *FakeWorkspace) GetPermissions(req Request) any {
107107
}
108108

109109
responseObjectID := fmt.Sprintf("/%s/%s", requestObjectType, objectId)
110+
111+
// V2 permissions APIs cascade-delete ACLs with the parent, so the cloud
112+
// returns 404 once the parent is gone. V1 APIs (jobs, pipelines, etc.)
113+
// retain ACL data after delete via async/soft delete; for those, we
114+
// fall through to the "empty ACL on miss" branch below, which is close
115+
// enough. New V2 resources should add a case to permissionsParentExists.
116+
if !s.permissionsParentExists(requestObjectType, objectId) {
117+
return Response{
118+
StatusCode: 404,
119+
Body: map[string]string{"message": fmt.Sprintf("%s %s not found.", requestObjectType, objectId)},
120+
}
121+
}
122+
110123
permissions, exists := s.Permissions[responseObjectID]
111124

112125
if !exists {
@@ -123,6 +136,23 @@ func (s *FakeWorkspace) GetPermissions(req Request) any {
123136
}
124137
}
125138

139+
// permissionsParentExists reports whether the parent object backing a
140+
// permissions request exists in workspace state. Returns true for resource
141+
// types without a parent-existence check wired up; V1 resources rely on
142+
// that fallback to keep their "empty ACL on miss" behavior.
143+
func (s *FakeWorkspace) permissionsParentExists(requestObjectType, objectId string) bool {
144+
switch requestObjectType {
145+
case "vector-search-endpoints":
146+
for _, ep := range s.VectorSearchEndpoints {
147+
if ep.Id == objectId {
148+
return true
149+
}
150+
}
151+
return false
152+
}
153+
return true
154+
}
155+
126156
func (s *FakeWorkspace) SetPermissions(req Request) any {
127157
defer s.LockUnlock()()
128158

0 commit comments

Comments
 (0)