From 6afc7b744daa420f3223c51df45a9bf0c4d6a9ca Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Thu, 16 Jul 2026 11:01:58 -0700 Subject: [PATCH] feat: add AIStor Memory API policy actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the memory:* policy action class (MemoryAction) used by MinIO AIStor's Memory API — cortex CRUD, secrets, and search on a Memory Bucket (Cortex). Mirrors the s3tables/s3vectors service-action classes: action constants + SupportedMemoryActions + IsValid + a condition-key map, wired into statement validation (action-type exclusivity, isMemory, ValidateMemory) and resource validation (a cortex is an S3 bucket, so resources use S3 ARN format). --- policy/actionset.go | 23 +++++++++ policy/memory-action.go | 95 ++++++++++++++++++++++++++++++++++++ policy/memory-action_test.go | 56 +++++++++++++++++++++ policy/resourceset.go | 16 ++++++ policy/statement.go | 55 ++++++++++++++++++++- 5 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 policy/memory-action.go create mode 100644 policy/memory-action_test.go diff --git a/policy/actionset.go b/policy/actionset.go index 0acd4a37..525f6eac 100644 --- a/policy/actionset.go +++ b/policy/actionset.go @@ -228,6 +228,19 @@ func (actionSet ActionSet) ToVectorsSlice() []VectorsAction { return actions } +// ToMemorySlice - returns slice of Memory actions from the action set. +func (actionSet ActionSet) ToMemorySlice() []MemoryAction { + if len(actionSet) == 0 { + return nil + } + actions := make([]MemoryAction, 0, len(actionSet)) + for action := range actionSet { + actions = append(actions, MemoryAction(action)) + } + + return actions +} + // UnmarshalJSON - decodes JSON data to ActionSet. func (actionSet *ActionSet) UnmarshalJSON(data []byte) error { var sset set.StringSet @@ -293,6 +306,16 @@ func (actionSet ActionSet) ValidateVectors() error { return nil } +// ValidateMemory checks if all actions are valid Memory actions +func (actionSet ActionSet) ValidateMemory() error { + for _, action := range actionSet.ToMemorySlice() { + if !action.IsValid() { + return Errorf("unsupported memory action '%v'", action) + } + } + return nil +} + // Validate checks if all actions are valid func (actionSet ActionSet) Validate() error { for _, action := range actionSet.ToSlice() { diff --git a/policy/memory-action.go b/policy/memory-action.go new file mode 100644 index 00000000..08c13ab7 --- /dev/null +++ b/policy/memory-action.go @@ -0,0 +1,95 @@ +// Copyright (c) 2015-2025 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package policy + +import ( + "github.com/minio/pkg/v3/policy/condition" +) + +// MemoryAction - AIStor Memory API policy action. The Memory API is a MinIO +// AIStor extension serving agent memory on a Cortex (Memory Bucket). +type MemoryAction string + +const ( + // MemoryCreateCortexAction - create a Memory cortex (Memory Bucket). + MemoryCreateCortexAction MemoryAction = "memory:CreateCortex" + + // MemoryDeleteCortexAction - delete a Memory cortex. + MemoryDeleteCortexAction = "memory:DeleteCortex" + + // MemoryGetCortexAction - read a Memory cortex's metadata. + MemoryGetCortexAction = "memory:GetCortex" + + // MemoryListCortexesAction - list Memory cortexes. + MemoryListCortexesAction = "memory:ListCortexes" + + // MemoryPutSecretAction - write a secret in a cortex. + MemoryPutSecretAction = "memory:PutSecret" + + // MemoryGetSecretAction - read a secret's decrypted value from a cortex. + MemoryGetSecretAction = "memory:GetSecret" + + // MemoryDeleteSecretAction - delete a secret from a cortex. + MemoryDeleteSecretAction = "memory:DeleteSecret" + + // MemoryListSecretsAction - list the secrets in a cortex. + MemoryListSecretsAction = "memory:ListSecrets" + + // MemorySearchAction - search (corpus-grep) the objects in a cortex. + MemorySearchAction = "memory:Search" + + // AllMemoryActions - all AIStor Memory API actions. + AllMemoryActions = "memory:*" +) + +// SupportedMemoryActions - list of all supported AIStor Memory API actions. +var SupportedMemoryActions = map[MemoryAction]struct{}{ + MemoryCreateCortexAction: {}, + MemoryDeleteCortexAction: {}, + MemoryGetCortexAction: {}, + MemoryListCortexesAction: {}, + MemoryPutSecretAction: {}, + MemoryGetSecretAction: {}, + MemoryDeleteSecretAction: {}, + MemoryListSecretsAction: {}, + MemorySearchAction: {}, + AllMemoryActions: {}, +} + +// IsValid - checks if action is valid or not. +func (action MemoryAction) IsValid() bool { + _, ok := SupportedMemoryActions[action] + return ok +} + +func createMemoryActionConditionKeyMap() map[Action]condition.KeySet { + commonKeys := []condition.Key{} + for _, keyName := range condition.CommonKeys { + commonKeys = append(commonKeys, keyName.ToKey()) + } + + memoryActionConditionKeyMap := map[Action]condition.KeySet{} + for act := range SupportedMemoryActions { + memoryActionConditionKeyMap[Action(act)] = condition.NewKeySet(commonKeys...) + } + + return memoryActionConditionKeyMap +} + +// MemoryActionConditionKeyMap - holds mapping of Memory actions to condition keys. +var MemoryActionConditionKeyMap = createMemoryActionConditionKeyMap() diff --git a/policy/memory-action_test.go b/policy/memory-action_test.go new file mode 100644 index 00000000..9d4365ab --- /dev/null +++ b/policy/memory-action_test.go @@ -0,0 +1,56 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package policy + +import ( + "testing" +) + +func TestMemoryActionIsValid(t *testing.T) { + testCases := []struct { + action MemoryAction + expectedResult bool + }{ + {MemoryCreateCortexAction, true}, + {MemoryDeleteCortexAction, true}, + {MemoryGetCortexAction, true}, + {MemoryListCortexesAction, true}, + {MemoryPutSecretAction, true}, + {MemoryGetSecretAction, true}, + {MemoryDeleteSecretAction, true}, + {MemoryListSecretsAction, true}, + {MemorySearchAction, true}, + {AllMemoryActions, true}, + {MemoryAction("memory:FooBar"), false}, + {MemoryAction("s3tables:CreateTable"), false}, + } + + for i, testCase := range testCases { + if result := testCase.action.IsValid(); result != testCase.expectedResult { + t.Fatalf("case %v: action %v: expected: %v, got: %v", i+1, testCase.action, testCase.expectedResult, result) + } + } +} + +func TestMemoryActionConditionKeys(t *testing.T) { + for action := range SupportedMemoryActions { + if _, ok := MemoryActionConditionKeyMap[Action(action)]; !ok { + t.Fatalf("action %v: no condition key set registered", action) + } + } +} diff --git a/policy/resourceset.go b/policy/resourceset.go index 7eb9966d..837ab832 100644 --- a/policy/resourceset.go +++ b/policy/resourceset.go @@ -207,6 +207,22 @@ func (resourceSet ResourceSet) ValidateVectors() error { return nil } +// ValidateMemory - validates ResourceSet for the AIStor Memory API. +// A Memory cortex is an S3 bucket, so resources use S3 ARN format +// (e.g., arn:aws:s3:::my-cortex or arn:aws:s3:::my-cortex/*). +func (resourceSet ResourceSet) ValidateMemory() error { + for resource := range resourceSet { + if !resource.isS3() { + return Errorf("resource '%v' type is not S3", resource) + } + if err := resource.Validate(); err != nil { + return err + } + } + + return nil +} + // ValidateBucket - validates ResourceSet is for given bucket or not. func (resourceSet ResourceSet) ValidateBucket(bucketName string) error { for resource := range resourceSet { diff --git a/policy/statement.go b/policy/statement.go index 04d32a96..6418e835 100644 --- a/policy/statement.go +++ b/policy/statement.go @@ -146,7 +146,7 @@ func (statement Statement) validateActionTypes() error { if len(actions) == 0 { actions = statement.NotActions } - var hasS3, hasAdmin, hasSTS, hasKMS, hasTable, hasVectors bool + var hasS3, hasAdmin, hasSTS, hasKMS, hasTable, hasVectors, hasMemory bool for action := range actions { switch { case AdminAction(action).IsValid(): @@ -159,12 +159,14 @@ func (statement Statement) validateActionTypes() error { hasTable = true case VectorsAction(action).IsValid(): hasVectors = true + case MemoryAction(action).IsValid(): + hasMemory = true default: hasS3 = true } } count := 0 - for _, b := range []bool{hasS3, hasAdmin, hasSTS, hasKMS, hasTable, hasVectors} { + for _, b := range []bool{hasS3, hasAdmin, hasSTS, hasKMS, hasTable, hasVectors, hasMemory} { if b { count++ } @@ -231,6 +233,15 @@ func (statement Statement) isVectors() bool { return false } +func (statement Statement) isMemory() bool { + for action := range statement.Actions { + if MemoryAction(action).IsValid() { + return true + } + } + return false +} + // isValid - checks whether statement is valid or not. func (statement Statement) isValid() error { if !statement.Effect.IsValid() { @@ -360,6 +371,46 @@ func (statement Statement) isValid() error { return nil } + if statement.isMemory() { + if err := statement.Actions.ValidateMemory(); err != nil { + return err + } + for action := range statement.Actions { + keys := statement.Conditions.Keys() + keyDiff := keys.Difference(MemoryActionConditionKeyMap[action]) + if !keyDiff.IsEmpty() { + return Errorf("unsupported condition keys '%v' used for action '%v'", keyDiff, action) + } + } + + if len(statement.Resources) == 0 && len(statement.NotResources) == 0 { + return Errorf("Resource must not be empty") + } + + if len(statement.Resources) > 0 && len(statement.NotResources) > 0 { + return Errorf("Resource and NotResource cannot be specified in the same statement") + } + + if err := statement.Resources.ValidateMemory(); err != nil { + return err + } + + if err := statement.NotResources.ValidateMemory(); err != nil { + return err + } + + for action := range statement.Actions { + if len(statement.Resources) > 0 && !statement.Resources.ObjectResourceExists() && !statement.Resources.BucketResourceExists() { + return Errorf("unsupported Resource found %v for action %v", statement.Resources, action) + } + if len(statement.NotResources) > 0 && !statement.NotResources.ObjectResourceExists() && !statement.NotResources.BucketResourceExists() { + return Errorf("unsupported NotResource found %v for action %v", statement.NotResources, action) + } + } + + return nil + } + if !statement.SID.IsValid() { return Errorf("invalid SID %v", statement.SID) }