Skip to content

Commit 70e9b7a

Browse files
fix: add S3 Vectors policy action validation support
- Add ToVectorsSlice() and ValidateVectors() methods to ActionSet - Add isVectors() method to Statement for detecting vectors actions - Add vectors validation block in Statement.isValid() - Add ValidateVectors() method to ResourceSet - Add tests for S3 Vectors policy parsing and action matching
1 parent 6885485 commit 70e9b7a

File tree

4 files changed

+316
-0
lines changed

4 files changed

+316
-0
lines changed

policy/actionset.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,19 @@ func (actionSet ActionSet) ToTableSlice() []TableAction {
215215
return actions
216216
}
217217

218+
// ToVectorsSlice - returns slice of vectors actions from the action set.
219+
func (actionSet ActionSet) ToVectorsSlice() []VectorsAction {
220+
if len(actionSet) == 0 {
221+
return nil
222+
}
223+
actions := make([]VectorsAction, 0, len(actionSet))
224+
for action := range actionSet {
225+
actions = append(actions, VectorsAction(action))
226+
}
227+
228+
return actions
229+
}
230+
218231
// UnmarshalJSON - decodes JSON data to ActionSet.
219232
func (actionSet *ActionSet) UnmarshalJSON(data []byte) error {
220233
var sset set.StringSet
@@ -270,6 +283,16 @@ func (actionSet ActionSet) ValidateTable() error {
270283
return nil
271284
}
272285

286+
// ValidateVectors checks if all actions are valid Vectors actions
287+
func (actionSet ActionSet) ValidateVectors() error {
288+
for _, action := range actionSet.ToVectorsSlice() {
289+
if !action.IsValid() {
290+
return Errorf("unsupported vectors action '%v'", action)
291+
}
292+
}
293+
return nil
294+
}
295+
273296
// Validate checks if all actions are valid
274297
func (actionSet ActionSet) Validate() error {
275298
for _, action := range actionSet.ToSlice() {

policy/policy_test.go

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2487,3 +2487,232 @@ func TestS3TablesActionsWithImplicitMatching(t *testing.T) {
24872487
})
24882488
}
24892489
}
2490+
2491+
func TestPolicyParseS3VectorsExamples(t *testing.T) {
2492+
tests := []struct {
2493+
name string
2494+
policyJSON string
2495+
expectedActions []Action
2496+
expectedResources []string
2497+
}{
2498+
{
2499+
name: "VectorBucketFullAccess",
2500+
policyJSON: `{
2501+
"Version": "2012-10-17",
2502+
"Statement": [
2503+
{
2504+
"Effect": "Allow",
2505+
"Action": [
2506+
"s3vectors:CreateVectorBucket",
2507+
"s3vectors:DeleteVectorBucket",
2508+
"s3vectors:GetVectorBucket",
2509+
"s3vectors:ListVectorBuckets"
2510+
],
2511+
"Resource": "arn:aws:s3:::vectors-bucket/*"
2512+
}
2513+
]
2514+
}`,
2515+
expectedActions: []Action{
2516+
Action(S3VectorsCreateVectorBucketAction),
2517+
Action(S3VectorsDeleteVectorBucketAction),
2518+
Action(S3VectorsGetVectorBucketAction),
2519+
Action(S3VectorsListVectorBucketsAction),
2520+
},
2521+
expectedResources: []string{"arn:aws:s3:::vectors-bucket/*"},
2522+
},
2523+
{
2524+
name: "IndexOperations",
2525+
policyJSON: `{
2526+
"Version": "2012-10-17",
2527+
"Statement": [
2528+
{
2529+
"Effect": "Allow",
2530+
"Action": [
2531+
"s3vectors:CreateIndex",
2532+
"s3vectors:DeleteIndex",
2533+
"s3vectors:GetIndex",
2534+
"s3vectors:ListIndexes"
2535+
],
2536+
"Resource": "arn:aws:s3:::vectors-bucket/*"
2537+
}
2538+
]
2539+
}`,
2540+
expectedActions: []Action{
2541+
Action(S3VectorsCreateIndexAction),
2542+
Action(S3VectorsDeleteIndexAction),
2543+
Action(S3VectorsGetIndexAction),
2544+
Action(S3VectorsListIndexesAction),
2545+
},
2546+
expectedResources: []string{"arn:aws:s3:::vectors-bucket/*"},
2547+
},
2548+
{
2549+
name: "VectorDataOperations",
2550+
policyJSON: `{
2551+
"Version": "2012-10-17",
2552+
"Statement": [
2553+
{
2554+
"Effect": "Allow",
2555+
"Action": [
2556+
"s3vectors:PutVectors",
2557+
"s3vectors:GetVectors",
2558+
"s3vectors:DeleteVectors",
2559+
"s3vectors:ListVectors",
2560+
"s3vectors:QueryVectors"
2561+
],
2562+
"Resource": "arn:aws:s3:::vectors-bucket/*"
2563+
}
2564+
]
2565+
}`,
2566+
expectedActions: []Action{
2567+
Action(S3VectorsPutVectorsAction),
2568+
Action(S3VectorsGetVectorsAction),
2569+
Action(S3VectorsDeleteVectorsAction),
2570+
Action(S3VectorsListVectorsAction),
2571+
Action(S3VectorsQueryVectorsAction),
2572+
},
2573+
expectedResources: []string{"arn:aws:s3:::vectors-bucket/*"},
2574+
},
2575+
{
2576+
name: "AllVectorsActions",
2577+
policyJSON: `{
2578+
"Version": "2012-10-17",
2579+
"Statement": [
2580+
{
2581+
"Effect": "Allow",
2582+
"Action": "s3vectors:*",
2583+
"Resource": "arn:aws:s3:::vectors-bucket/*"
2584+
}
2585+
]
2586+
}`,
2587+
expectedActions: []Action{Action(AllS3VectorsActions)},
2588+
expectedResources: []string{"arn:aws:s3:::vectors-bucket/*"},
2589+
},
2590+
}
2591+
2592+
for _, tt := range tests {
2593+
t.Run(tt.name, func(t *testing.T) {
2594+
p, err := ParseConfig(strings.NewReader(tt.policyJSON))
2595+
if err != nil {
2596+
t.Fatalf("failed to parse policy: %v", err)
2597+
}
2598+
2599+
if len(p.Statements) != 1 {
2600+
t.Fatalf("expected 1 statement, got %d", len(p.Statements))
2601+
}
2602+
2603+
stmt := p.Statements[0]
2604+
2605+
// Check actions
2606+
if len(stmt.Actions) != len(tt.expectedActions) {
2607+
t.Errorf("expected %d actions, got %d", len(tt.expectedActions), len(stmt.Actions))
2608+
}
2609+
for _, expectedAction := range tt.expectedActions {
2610+
if !stmt.Actions.Contains(expectedAction) {
2611+
t.Errorf("expected action %v not found in statement", expectedAction)
2612+
}
2613+
}
2614+
2615+
// Check resources
2616+
if len(stmt.Resources) != len(tt.expectedResources) {
2617+
t.Errorf("expected %d resources, got %d", len(tt.expectedResources), len(stmt.Resources))
2618+
}
2619+
for _, expectedResource := range tt.expectedResources {
2620+
found := false
2621+
for r := range stmt.Resources {
2622+
if r.String() == expectedResource {
2623+
found = true
2624+
break
2625+
}
2626+
}
2627+
if !found {
2628+
t.Errorf("expected resource %v not found in statement", expectedResource)
2629+
}
2630+
}
2631+
})
2632+
}
2633+
}
2634+
2635+
func TestS3VectorsActionsAllowed(t *testing.T) {
2636+
policyJSON := `{
2637+
"Version": "2012-10-17",
2638+
"Statement": [
2639+
{
2640+
"Effect": "Allow",
2641+
"Action": ["s3vectors:*"],
2642+
"Resource": ["arn:aws:s3:::vectors-bucket/*"]
2643+
}
2644+
]
2645+
}`
2646+
2647+
testCases := []struct {
2648+
name string
2649+
args Args
2650+
expectedResult bool
2651+
description string
2652+
}{
2653+
{
2654+
name: "CreateVectorBucket allowed",
2655+
args: Args{
2656+
Action: Action(S3VectorsCreateVectorBucketAction),
2657+
BucketName: "vectors-bucket",
2658+
ObjectName: "my-vector-bucket",
2659+
},
2660+
expectedResult: true,
2661+
description: "CreateVectorBucket should be allowed with s3vectors:*",
2662+
},
2663+
{
2664+
name: "ListIndexes allowed",
2665+
args: Args{
2666+
Action: Action(S3VectorsListIndexesAction),
2667+
BucketName: "vectors-bucket",
2668+
ObjectName: "my-vector-bucket/indexes",
2669+
},
2670+
expectedResult: true,
2671+
description: "ListIndexes should be allowed with s3vectors:*",
2672+
},
2673+
{
2674+
name: "PutVectors allowed",
2675+
args: Args{
2676+
Action: Action(S3VectorsPutVectorsAction),
2677+
BucketName: "vectors-bucket",
2678+
ObjectName: "my-vector-bucket/index/my-index",
2679+
},
2680+
expectedResult: true,
2681+
description: "PutVectors should be allowed with s3vectors:*",
2682+
},
2683+
{
2684+
name: "QueryVectors allowed",
2685+
args: Args{
2686+
Action: Action(S3VectorsQueryVectorsAction),
2687+
BucketName: "vectors-bucket",
2688+
ObjectName: "my-vector-bucket/index/my-index",
2689+
},
2690+
expectedResult: true,
2691+
description: "QueryVectors should be allowed with s3vectors:*",
2692+
},
2693+
{
2694+
name: "Wrong bucket not allowed",
2695+
args: Args{
2696+
Action: Action(S3VectorsCreateVectorBucketAction),
2697+
BucketName: "wrong-bucket",
2698+
ObjectName: "my-vector-bucket",
2699+
},
2700+
expectedResult: false,
2701+
description: "CreateVectorBucket should not be allowed on wrong bucket",
2702+
},
2703+
}
2704+
2705+
for _, tc := range testCases {
2706+
t.Run(tc.name, func(t *testing.T) {
2707+
p, err := ParseConfig(strings.NewReader(policyJSON))
2708+
if err != nil {
2709+
t.Fatalf("failed to parse policy: %v", err)
2710+
}
2711+
2712+
result := p.IsAllowed(tc.args)
2713+
if result != tc.expectedResult {
2714+
t.Errorf("%s: expected %v, got %v", tc.description, tc.expectedResult, result)
2715+
}
2716+
})
2717+
}
2718+
}

policy/resourceset.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,21 @@ func (resourceSet ResourceSet) ValidateTable() error {
192192
return nil
193193
}
194194

195+
// ValidateVectors - validates ResourceSet for S3 Vectors.
196+
// S3 Vectors uses S3 ARN format for resources (e.g., arn:aws:s3:::vectors-bucket/*).
197+
func (resourceSet ResourceSet) ValidateVectors() error {
198+
for resource := range resourceSet {
199+
if !resource.isS3() {
200+
return Errorf("resource '%v' type is not S3", resource)
201+
}
202+
if err := resource.Validate(); err != nil {
203+
return err
204+
}
205+
}
206+
207+
return nil
208+
}
209+
195210
// ValidateBucket - validates ResourceSet is for given bucket or not.
196211
func (resourceSet ResourceSet) ValidateBucket(bucketName string) error {
197212
for resource := range resourceSet {

policy/statement.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,15 @@ func (statement Statement) isTable() bool {
171171
return false
172172
}
173173

174+
func (statement Statement) isVectors() bool {
175+
for action := range statement.Actions {
176+
if VectorsAction(action).IsValid() {
177+
return true
178+
}
179+
}
180+
return false
181+
}
182+
174183
// isValid - checks whether statement is valid or not.
175184
func (statement Statement) isValid() error {
176185
if !statement.Effect.IsValid() {
@@ -266,6 +275,46 @@ func (statement Statement) isValid() error {
266275
return nil
267276
}
268277

278+
if statement.isVectors() {
279+
if err := statement.Actions.ValidateVectors(); err != nil {
280+
return err
281+
}
282+
for action := range statement.Actions {
283+
keys := statement.Conditions.Keys()
284+
keyDiff := keys.Difference(VectorsActionConditionKeyMap[action])
285+
if !keyDiff.IsEmpty() {
286+
return Errorf("unsupported condition keys '%v' used for action '%v'", keyDiff, action)
287+
}
288+
}
289+
290+
if len(statement.Resources) == 0 && len(statement.NotResources) == 0 {
291+
return Errorf("Resource must not be empty")
292+
}
293+
294+
if len(statement.Resources) > 0 && len(statement.NotResources) > 0 {
295+
return Errorf("Resource and NotResource cannot be specified in the same statement")
296+
}
297+
298+
if err := statement.Resources.ValidateVectors(); err != nil {
299+
return err
300+
}
301+
302+
if err := statement.NotResources.ValidateVectors(); err != nil {
303+
return err
304+
}
305+
306+
for action := range statement.Actions {
307+
if len(statement.Resources) > 0 && !statement.Resources.ObjectResourceExists() && !statement.Resources.BucketResourceExists() {
308+
return Errorf("unsupported Resource found %v for action %v", statement.Resources, action)
309+
}
310+
if len(statement.NotResources) > 0 && !statement.NotResources.ObjectResourceExists() && !statement.NotResources.BucketResourceExists() {
311+
return Errorf("unsupported NotResource found %v for action %v", statement.NotResources, action)
312+
}
313+
}
314+
315+
return nil
316+
}
317+
269318
if !statement.SID.IsValid() {
270319
return Errorf("invalid SID %v", statement.SID)
271320
}

0 commit comments

Comments
 (0)