Expose permission-checked immutable asset metadata for shared depot adoption
This commit is contained in:
@@ -271,6 +271,51 @@ func assetReadable(r *http.Request, tx *sql.Tx, project, workspace, id, account
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Metadata uses the same read policy as original bytes; it is not a persistent grant.
|
||||
func (s *service) getAssetMetadata(w http.ResponseWriter, r *http.Request, project, account string) error {
|
||||
if r.Method != http.MethodGet {
|
||||
reject(w, 405, "method_not_allowed")
|
||||
return nil
|
||||
}
|
||||
id, workspace := r.PathValue("asset"), r.PathValue("workspace")
|
||||
if !uuidSlugPattern.MatchString(id) {
|
||||
reject(w, 404, "not_found")
|
||||
return nil
|
||||
}
|
||||
tx, err := s.db.BeginTx(r.Context(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var locked string
|
||||
err = tx.QueryRowContext(r.Context(), `SELECT id::text FROM workspaces WHERE project=$1 AND id=$2 FOR UPDATE`, project, workspace).Scan(&locked)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allowed, err := assetReadable(r, tx, project, workspace, id, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !allowed {
|
||||
reject(w, 404, "not_found")
|
||||
return nil
|
||||
}
|
||||
var info assetInfo
|
||||
var digest string
|
||||
err = tx.QueryRowContext(r.Context(), `SELECT id::text,mime_type,octet_length(bytes),width,height,digest FROM canvas_assets WHERE project=$1 AND workspace_id=$2 AND id=$3`, project, workspace, id).Scan(&info.ID, &info.MIME, &info.Bytes, &info.Width, &info.Height, &digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
info.URL = s.assetURL(project, workspace, id)
|
||||
w.Header().Set("Cache-Control", "private, no-store")
|
||||
reply(w, 200, response{"metadata_version": 1, "asset": info, "sha256": digest})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) getAsset(w http.ResponseWriter, r *http.Request, project, account string) error {
|
||||
if r.Method != http.MethodGet {
|
||||
reject(w, 405, "method_not_allowed")
|
||||
|
||||
@@ -151,6 +151,7 @@ func Register(mux *http.ServeMux, db *sql.DB, callbacks map[string]string, origi
|
||||
mux.HandleFunc("/projects/{project}/workspaces/{workspace}/materials", s.protected(callbacks, s.materials))
|
||||
mux.HandleFunc("/projects/{project}/workspaces/{workspace}/assets", s.protected(callbacks, s.uploadAsset))
|
||||
mux.HandleFunc("/projects/{project}/workspaces/{workspace}/assets/operations/{request}", s.protected(callbacks, s.assetOperation))
|
||||
mux.HandleFunc("/projects/{project}/workspaces/{workspace}/assets/{asset}/metadata", s.protected(callbacks, s.getAssetMetadata))
|
||||
mux.HandleFunc("/projects/{project}/workspaces/{workspace}/assets/{asset}", s.protected(callbacks, s.getAsset))
|
||||
mux.HandleFunc("/projects/{project}/workspaces/{workspace}/materials/{material}", s.protected(callbacks, s.material))
|
||||
mux.HandleFunc("/projects/{project}/workspaces/{workspace}/materials/{material}/canvas", s.protected(callbacks, s.canvas))
|
||||
|
||||
@@ -584,9 +584,33 @@ func TestOnboardingAndSharing(t *testing.T) {
|
||||
t.Fatal("preview changed small original or privacy headers")
|
||||
}
|
||||
}
|
||||
// Metadata discloses the same immutable identity only after a fresh read permission check.
|
||||
metadataPath := workspacePath + "/assets/" + uploaded["id"].(string) + "/metadata"
|
||||
for _, sample := range []struct {
|
||||
token string
|
||||
status int
|
||||
}{{tokens[0], 200}, {tokens[1], 404}, {tokens[2], 404}} {
|
||||
request := httptest.NewRequest("GET", metadataPath, nil)
|
||||
request.Header.Set("Authorization", "Bearer "+sample.token)
|
||||
result := httptest.NewRecorder()
|
||||
mux.ServeHTTP(result, request)
|
||||
if result.Code != sample.status {
|
||||
t.Fatalf("metadata permission got %d want %d: %s", result.Code, sample.status, result.Body.String())
|
||||
}
|
||||
if result.Code == 200 {
|
||||
var metadata response
|
||||
if err := json.Unmarshal(result.Body.Bytes(), &metadata); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest := sha256.Sum256(assetPNG.Bytes())
|
||||
if metadata["metadata_version"] != float64(1) || metadata["sha256"] != hex.EncodeToString(digest[:]) || !modelEqual(metadata["asset"], uploaded) || result.Header().Get("Cache-Control") != "private, no-store" {
|
||||
t.Fatalf("invalid metadata identity: %s", result.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
// Neither a snapshot nor an asset response may keep the workspace locked
|
||||
// while a slow downstream consumer is accepting its body.
|
||||
for _, path := range []string{canvasPath, workspacePath + "/assets/" + uploaded["id"].(string)} {
|
||||
for _, path := range []string{canvasPath, workspacePath + "/assets/" + uploaded["id"].(string), metadataPath} {
|
||||
var probeErr error
|
||||
writer := &readLockProbe{ResponseRecorder: httptest.NewRecorder(), probe: func() {
|
||||
lockTx, e := db.BeginTx(ctx, nil)
|
||||
|
||||
Reference in New Issue
Block a user