s3vectors: put, get and delete vector bucket policies (#12201)

* util: do not bind outbound dials to a loopback -ip.bind

`weed server -ip.bind=127.0.0.1` keeps every listener local, but the
same address became the source of outbound connections, so the filer
dialled its remote mount from 127.0.0.1 and failed with "connect:
invalid argument". A loopback source cannot reach off the host; leave
the source to the OS, as for a wildcard bind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3vectors: put, get and delete vector bucket policies

The AWS S3 Vectors policy operations had no handlers; the policy document
is stored on the shared table bucket, so the handlers delegate to the
catalog's PutTableBucketPolicy, GetTableBucketPolicy and
DeleteTableBucketPolicy and keep its guard.

* s3vectors: treat ICEBERG buckets as absent in policy handlers

GetVectorBucketPolicy/PutVectorBucketPolicy/DeleteVectorBucketPolicy
delegated straight to the table bucket, so a bucket created for ICEBERG
tables — invisible to every other vectors operation — could still have
its policy read and written through the vectors API. Gate all three on
lanceBucket, the same format check DeleteVectorBucket and CreateIndex
already apply.

* test/vectors: cover vector bucket policy put/get/delete

Round-trips a policy document through the new ops and asserts a get on
the deleted policy surfaces NotFoundException, so the boto3 conformance
run exercises the added handlers end to end.

* s3vectors: validate the bucket ARN and policy document

Bucket-level operations now reject an index ARN and an ARN minted for
another service, and PutVectorBucketPolicy refuses a document carrying
s3vectors actions — the evaluator only honors s3tables actions, so such a
rule would be a dead grant.

* s3vectors: validate the bucket ARN on the remaining bucket-level operations

* s3vectors: cover bucket ARN and policy document validation

TestBucketARNs pins the accepted forms and the index/foreign-service
rejections; TestValidatePolicyDocument covers a valid document, a
malformed one, and the dead s3vectors action namespace.

* s3vectors test: use an enforceable action in the bucket policy test

The catalog refuses s3vectors actions in a stored policy, so the put call
rejected the document before the get and delete assertions ran.

---------

Co-authored-by: Ólafur Bjarki Bogason <olafur@genkiinstruments.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-29 11:15:59 -07:00
1 parent 8e44178dc6
commit eecc461b39
7 files changed
+261 -22

No files matched your search

+10
View File
@@ -60,6 +60,11 @@ func (h *S3TablesHandler) handlePutTableBucketPolicy(w http.ResponseWriter, r *h
return fmt.Errorf("resourcePolicy is required")
}
if err := validatePolicyDocument(req.ResourcePolicy); err != nil {
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
return err
}
bucketName, err := parseBucketNameFromARN(req.TableBucketARN)
if err != nil {
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
@@ -320,6 +325,11 @@ func (h *S3TablesHandler) handlePutTablePolicy(w http.ResponseWriter, r *http.Re
return fmt.Errorf("resourcePolicy is required")
}
if err := validatePolicyDocument(req.ResourcePolicy); err != nil {
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
return err
}
bucketName, err := parseBucketNameFromARN(req.TableBucketARN)
if err != nil {
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
+35
View File
@@ -141,6 +141,41 @@ func CheckTableOrBucketPermission(operation, principal, resourceOwner, resourceP
return resourceOK || bucketOK
}
// validatePolicyDocument rejects a policy the evaluator could not honor: one
// that does not parse, or whose actions live in a namespace that never
// matches. An s3vectors action stored here would be a dead rule the writer
// believes is enforced — vector requests authorize their s3tables action.
func validatePolicyDocument(raw string) error {
var doc PolicyDocument
if err := json.Unmarshal([]byte(raw), &doc); err != nil {
return fmt.Errorf("invalid policy document: %w", err)
}
for _, stmt := range doc.Statement {
for _, action := range statementActions(stmt.Action) {
if strings.HasPrefix(action, "s3vectors:") {
return fmt.Errorf("action %q is not enforceable in a resource policy", action)
}
}
}
return nil
}
func statementActions(spec interface{}) []string {
switch a := spec.(type) {
case string:
return []string{a}
case []interface{}:
var actions []string
for _, item := range a {
if str, ok := item.(string); ok {
actions = append(actions, str)
}
}
return actions
}
return nil
}
// deniedByPolicy reports whether another applicable resource policy vetoes the
// request. The admin account is exempt, matching CheckPermissionWithContext.
func deniedByPolicy(operation, principal, resourcePolicy, resourceARN string, ctx *PolicyContext) bool {
+18
View File
@@ -428,3 +428,21 @@ func TestCheckPermissionLegacyPolicyPrincipalShapes(t *testing.T) {
t.Error("legacy Allow with array principal must still match the scoped caller")
}
}
func TestValidatePolicyDocument(t *testing.T) {
valid := `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":"s3tables:GetTable","Resource":"*"}]}`
if err := validatePolicyDocument(valid); err != nil {
t.Fatalf("valid policy rejected: %v", err)
}
if err := validatePolicyDocument("not-json"); err == nil {
t.Fatal("malformed policy should be rejected")
}
dead := `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":"s3vectors:PutVectors","Resource":"*"}]}`
if err := validatePolicyDocument(dead); err == nil {
t.Fatal("s3vectors action should be rejected")
}
deadList := `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":["s3tables:*","s3vectors:*"],"Resource":"*"}]}`
if err := validatePolicyDocument(deadList); err == nil {
t.Fatal("action list containing s3vectors should be rejected")
}
}