Verify asset permission revocation and immutable adoption rejection receipts
This commit is contained in:
@@ -71,7 +71,7 @@ jobs:
|
||||
PYSLACK
|
||||
- name: Fetch pinned headless HTTP integration client
|
||||
env:
|
||||
HEADLESS_CLIENT_REVISION: a626e6c3006765ae5569b40cf7d48b7f263a722a
|
||||
HEADLESS_CLIENT_REVISION: 902e30a65b07a97304ffc41fbbc7a9d1a91d08fd
|
||||
run: |
|
||||
set -eu
|
||||
mkdir -p .headless-client
|
||||
|
||||
@@ -706,6 +706,62 @@ func TestOnboardingAndSharing(t *testing.T) {
|
||||
t.Fatalf("stable URL lost child asset binding: %v count=%d", err, managedBindings)
|
||||
}
|
||||
call("POST", assetCanvasPath+"/subgraphs/asset-frame/dissolve", tokens[0], response{"expected_versions": map[string]int64{assetBoard["id"].(string): 5, assetChild: 2}}, 200, "", accept, "40000000-0000-4000-8000-000000000003")
|
||||
// A warm metadata/original cache is not a future permission grant. An external
|
||||
// collaborator can read through a source board grant, but a new canonical
|
||||
// target write must recheck asset readability after that grant is revoked.
|
||||
sourceGrantPath := workspacePath + "/materials/" + assetBoard["id"].(string) + "/grants"
|
||||
call("POST", sourceGrantPath, tokens[0], response{"account_id": accounts[2], "role": "viewer"}, 204)
|
||||
call("GET", metadataPath, tokens[2], nil, 200)
|
||||
download(tokens[2], 200)
|
||||
targetBoard := call("POST", workspacePath+"/materials", tokens[0], response{"kind": "board", "title": "Asset adoption target", "content": response{}}, 201)
|
||||
targetID := targetBoard["id"].(string)
|
||||
targetPath := workspacePath + "/materials/" + targetID
|
||||
targetGrantPath := targetPath + "/grants"
|
||||
call("POST", targetGrantPath, tokens[0], response{"account_id": accounts[2], "role": "editor"}, 204)
|
||||
migrationID := modelTestID(9200)
|
||||
call("POST", targetPath+"/canvas/model/migrate", tokens[0], response{"protocol_version": 2, "request_id": migrationID, "expected_versions": map[string]int64{targetID: 1}}, 200, "", accept, migrationID)
|
||||
targetModel := call("GET", targetPath+"/canvas/model", tokens[2], nil, 200)["model"].(map[string]any)
|
||||
targetVersion := int64(targetModel["versions"].(map[string]any)[targetID].(float64))
|
||||
adoptedElement := modelTestNode(modelTestID(9201), targetID)
|
||||
adoptedElement["componentId"] = "image"
|
||||
adoptedElement["data"] = map[string]any{"title": "Adopted", "text": "", "assetId": uploaded["id"], "src": uploaded["url"]}
|
||||
adoptionRequest := func(requestID, operationID string) modelBatch {
|
||||
return modelBatch{ProtocolVersion: 2, RequestID: requestID, Expected: map[string]int64{targetID: targetVersion}, Operations: []modelOperation{{ID: operationID, Changes: []modelChange{{Kind: "element", ID: modelString(adoptedElement["id"]), Before: modelJSON(nil), After: modelJSON(adoptedElement)}}}}}
|
||||
}
|
||||
call("DELETE", sourceGrantPath+"/"+accounts[2], tokens[0], nil, 204)
|
||||
call("GET", metadataPath, tokens[2], nil, 404)
|
||||
download(tokens[2], 404)
|
||||
deniedID := modelTestID(9202)
|
||||
deniedBody := adoptionRequest(deniedID, modelTestID(9203))
|
||||
denied := call("POST", targetPath+"/canvas/model/operations", tokens[2], deniedBody, 400, "", accept, deniedID)
|
||||
if denied["status"] != "not-applied" || denied["terminal"] != true || denied["error"] != "invalid_asset_reference" {
|
||||
t.Fatalf("revoked source did not yield a definitive no-write receipt: %v", denied)
|
||||
}
|
||||
unchanged := call("GET", targetPath+"/canvas/model", tokens[2], nil, 200)["model"].(map[string]any)
|
||||
if !modelEqual(unchanged, targetModel) {
|
||||
t.Fatal("revoked-source adoption changed target model or versions")
|
||||
}
|
||||
var targetBindings int
|
||||
if err := db.QueryRowContext(ctx, "SELECT count(*) FROM canvas_asset_bindings WHERE project=$1 AND asset_id=$2 AND board_id=$3", project, uploaded["id"], targetID).Scan(&targetBindings); err != nil || targetBindings != 0 {
|
||||
t.Fatalf("rejected adoption left a target binding: %d %v", targetBindings, err)
|
||||
}
|
||||
// Restoring source access does not change the frozen rejected request's result.
|
||||
call("POST", sourceGrantPath, tokens[0], response{"account_id": accounts[2], "role": "viewer"}, 204)
|
||||
call("GET", metadataPath, tokens[2], nil, 200)
|
||||
replayedDenial := call("POST", targetPath+"/canvas/model/operations", tokens[2], deniedBody, 400, "", accept, deniedID)
|
||||
if !modelEqual(denied, replayedDenial) {
|
||||
t.Fatal("permission restoration changed a terminal receipt")
|
||||
}
|
||||
acceptedID := modelTestID(9204)
|
||||
call("POST", targetPath+"/canvas/model/operations", tokens[2], adoptionRequest(acceptedID, modelTestID(9205)), 200, "", accept, acceptedID)
|
||||
call("DELETE", sourceGrantPath+"/"+accounts[2], tokens[0], nil, 204)
|
||||
// The independently authorized target now owns a binding; source revocation
|
||||
// must not revoke a resource still readable through that target.
|
||||
call("GET", metadataPath, tokens[2], nil, 200)
|
||||
download(tokens[2], 200)
|
||||
call("DELETE", targetGrantPath+"/"+accounts[2], tokens[0], nil, 204)
|
||||
call("GET", metadataPath, tokens[2], nil, 404)
|
||||
download(tokens[2], 404)
|
||||
download(tokens[1], 200)
|
||||
upload([]byte("not an image"), "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", 400)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user