diff --git a/weed/admin/view/app/object_store_users.templ b/weed/admin/view/app/object_store_users.templ index aa47ab539b..51e2e58546 100644 --- a/weed/admin/view/app/object_store_users.templ +++ b/weed/admin/view/app/object_store_users.templ @@ -230,6 +230,8 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { Write List Tagging + Read ACL (ReadAcp) + Write ACL (WriteAcp) Bypass Governance Retention Get Object Retention @@ -342,6 +344,8 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { Write List Tagging + Read ACL (ReadAcp) + Write ACL (WriteAcp) Bypass Governance Retention Get Object Retention @@ -405,6 +409,11 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { Hold Ctrl/Cmd to select multiple buckets + + Other Bucket Permissions + Grants the permission and bucket pickers above cannot show. Uncheck a grant to remove it. + + Attached Policies @@ -739,15 +748,10 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { isAdmin: false, permissions: [], applyToAll: false, - specificBuckets: [] + specificBuckets: [], + unshownActions: [] }; - // Check if user has Admin permission - if (actions.includes('Admin')) { - result.isAdmin = true; - return result; - } - // Separate bucket-scoped from global actions const bucketActions = []; const globalBucketPerms = []; @@ -756,44 +760,67 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { if (action.startsWith('s3tables:')) { const actionValue = action.slice('s3tables:'.length); if (actionValue === '*') { - globalBucketPerms.push('S3TablesAdmin'); + globalBucketPerms.push({ permission: 'S3TablesAdmin', action: action }); return; } const parts = actionValue.split(':'); const perm = parts[0]; - const bucket = parts.length > 1 ? parts.slice(1).join(':').replace(/\/\*$/, '') : ''; + const bucket = parts.length > 1 ? parts.slice(1).join(':') : ''; if (bucket) { - bucketActions.push({ permission: perm, bucketId: 's3tables:' + bucket }); + bucketActions.push({ permission: perm, bucketId: 's3tables:' + bucket, action: action }); } else { - globalBucketPerms.push(perm); + globalBucketPerms.push({ permission: perm, action: action }); } } else if (action.includes(':')) { const parts = action.split(':'); const perm = parts[0]; - const bucket = parts.slice(1).join(':').replace(/\/\*$/, ''); - bucketActions.push({ permission: perm, bucketId: 's3:' + bucket }); + const bucket = parts.slice(1).join(':'); + bucketActions.push({ permission: perm, bucketId: 's3:' + bucket, action: action }); } else { - globalBucketPerms.push(action); + globalBucketPerms.push({ permission: action, action: action }); } }); + const listed = globalBucketPerms.filter(gp => hasPickerOption('editActions', gp.permission)); + const unlisted = globalBucketPerms.filter(gp => !listed.includes(gp)).map(gp => gp.action); + + if (actions.includes('Admin')) { + result.isAdmin = true; + result.permissions = listed.map(gp => gp.permission); + result.unshownActions = unlisted.concat(bucketActions.map(ba => ba.action)); + return result; + } + // If we have global bucket permissions (no colon), they apply to all buckets if (globalBucketPerms.length > 0) { - result.permissions = globalBucketPerms; + result.permissions = listed.map(gp => gp.permission); result.applyToAll = true; + result.unshownActions = unlisted.concat(bucketActions.map(ba => ba.action)); } else if (bucketActions.length > 0) { - // Get unique permissions and buckets + // The form saves every permission on every selected bucket, so it can + // show the grants only when they cover that full product with listed options. const perms = [...new Set(bucketActions.map(ba => ba.permission))]; const buckets = [...new Set(bucketActions.map(ba => ba.bucketId))]; + const pairs = new Set(bucketActions.map(ba => JSON.stringify([ba.permission, ba.bucketId]))); + const allListed = perms.every(perm => hasPickerOption('editActions', perm)) && + buckets.every(bucketId => hasPickerOption('editSelectedBuckets', bucketId)); - result.permissions = perms; - result.applyToAll = false; - result.specificBuckets = buckets; + if (allListed && pairs.size === perms.length * buckets.length) { + result.permissions = perms; + result.specificBuckets = buckets; + } else { + result.unshownActions = bucketActions.map(ba => ba.action); + } } return result; } + function hasPickerOption(selectId, value) { + const select = document.getElementById(selectId); + return !!select && Array.from(select.options).some(option => option.value === value); + } + function parseBucketOptionValue(value) { if (value.startsWith('s3tables:')) { return { type: 's3tables', name: value.slice('s3tables:'.length) }; @@ -804,6 +831,32 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { return { type: 's3', name: value }; } + function renderUnshownActions(actions) { + const container = document.getElementById('editUnshownActions'); + container.replaceChildren(); + actions.forEach((action, i) => { + const wrapper = document.createElement('div'); + wrapper.className = 'form-check'; + const input = document.createElement('input'); + input.className = 'form-check-input'; + input.type = 'checkbox'; + input.id = 'editUnshownAction' + i; + input.value = action; + input.checked = true; + const label = document.createElement('label'); + label.className = 'form-check-label'; + label.htmlFor = input.id; + label.textContent = action; + wrapper.append(input, label); + container.appendChild(wrapper); + }); + document.getElementById('editUnshownActionsGroup').style.display = actions.length > 0 ? '' : 'none'; + } + + function selectedUnshownActions() { + return Array.from(document.querySelectorAll('#editUnshownActions input:checked')).map(input => input.value); + } + // Build bucket permission action strings using original permissions dropdown /** * Builds bucket permission strings based on selected permissions and bucket scope. @@ -943,6 +996,12 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { const user = await response.json(); // Populate edit form + renderUnshownActions([]); + document.getElementById('editAllBuckets').checked = true; + Array.from(document.getElementById('editSelectedBuckets').options).forEach(option => { + option.selected = false; + }); + toggleBucketList('edit'); document.getElementById('editUsername').value = username; document.getElementById('editEmail').value = user.email || ''; @@ -963,14 +1022,13 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { // Populate bucket permissions using original permissions dropdown if (user.actions && user.actions.length > 0) { const bucketPerms = parseBucketPermissions(user.actions); + renderUnshownActions(bucketPerms.unshownActions); // Set permissions in the original multi-select const actionsSelect = document.getElementById('editActions'); if (actionsSelect) { Array.from(actionsSelect.options).forEach(option => { - if (bucketPerms.isAdmin && option.value === 'Admin') { - option.selected = true; - } else if (!bucketPerms.isAdmin && bucketPerms.permissions.includes(option.value)) { + if (bucketPerms.permissions.includes(option.value)) { option.selected = true; } }); @@ -1232,15 +1290,15 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { return; } - // Validate that permissions are not empty - if (!allActions || allActions.length === 0) { + const actions = [...new Set([...allActions, ...selectedUnshownActions()])]; + if (actions.length === 0) { showAlert('At least one permission must be selected', 'error'); return; } const userData = { email: document.getElementById('editEmail').value, - actions: allActions, + actions: actions, policy_names: Array.from(document.getElementById('editPolicies').selectedOptions).map(option => option.value) }; diff --git a/weed/admin/view/app/object_store_users_templ.go b/weed/admin/view/app/object_store_users_templ.go index cb5fad0b2b..811ab4a986 100644 --- a/weed/admin/view/app/object_store_users_templ.go +++ b/weed/admin/view/app/object_store_users_templ.go @@ -237,7 +237,7 @@ func ObjectStoreUsers(data dash.ObjectStoreUsersData) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Email Permissions Admin (Full Access) Read Write List Tagging Bypass Governance Retention Get Object Retention Put Object Retention Get Object Legal Hold Put Object Legal Hold Get Bucket Object Lock Configuration Put Bucket Object Lock Configuration Put Bucket Policy Delete Bucket Policy S3 Tables Admin (Full Access) Create Table Bucket Get Table Bucket List Table Buckets Delete Table Bucket Put Table Bucket Policy Get Table Bucket Policy Delete Table Bucket Policy Create Namespace Get Namespace List Namespaces Delete Namespace Create Table Get Table List Tables Delete Table Put Table Policy Get Table Policy Delete Table Policy Tag Resource List Tags Untag Resource Hold Ctrl/Cmd to select multiple permissionsBucket Scope Apply selected permissions to specific buckets or all buckets All Buckets Specific Buckets Hold Ctrl/Cmd to select multiple bucketsAttached Policies Hold Ctrl/Cmd to select multiple policies Generate access key automaticallyEdit UserEmail Permissions Admin (Full Access) Read Write List Tagging Bypass Governance Retention Get Object Retention Put Object Retention Get Object Legal Hold Put Object Legal Hold Get Bucket Object Lock Configuration Put Bucket Object Lock Configuration Put Bucket Policy Delete Bucket Policy S3 Tables Admin (Full Access) Create Table Bucket Get Table Bucket List Table Buckets Delete Table Bucket Put Table Bucket Policy Get Table Bucket Policy Delete Table Bucket Policy Create Namespace Get Namespace List Namespaces Delete Namespace Create Table Get Table List Tables Delete Table Put Table Policy Get Table Policy Delete Table Policy Tag Resource List Tags Untag ResourceBucket Scope Apply selected permissions to specific buckets or all buckets All Buckets Specific Buckets Hold Ctrl/Cmd to select multiple bucketsAttached Policies GroupsAdd to group... User DetailsManage Access KeysAccess Keys for Create New KeyLeave blank to auto-generate.Access Key Secret Key Create Cancel") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Email Permissions Admin (Full Access) Read Write List Tagging Read ACL (ReadAcp) Write ACL (WriteAcp) Bypass Governance Retention Get Object Retention Put Object Retention Get Object Legal Hold Put Object Legal Hold Get Bucket Object Lock Configuration Put Bucket Object Lock Configuration Put Bucket Policy Delete Bucket Policy S3 Tables Admin (Full Access) Create Table Bucket Get Table Bucket List Table Buckets Delete Table Bucket Put Table Bucket Policy Get Table Bucket Policy Delete Table Bucket Policy Create Namespace Get Namespace List Namespaces Delete Namespace Create Table Get Table List Tables Delete Table Put Table Policy Get Table Policy Delete Table Policy Tag Resource List Tags Untag Resource Hold Ctrl/Cmd to select multiple permissionsBucket Scope Apply selected permissions to specific buckets or all buckets All Buckets Specific Buckets Hold Ctrl/Cmd to select multiple bucketsAttached Policies Hold Ctrl/Cmd to select multiple policies Generate access key automaticallyEdit UserEmail Permissions Admin (Full Access) Read Write List Tagging Read ACL (ReadAcp) Write ACL (WriteAcp) Bypass Governance Retention Get Object Retention Put Object Retention Get Object Legal Hold Put Object Legal Hold Get Bucket Object Lock Configuration Put Bucket Object Lock Configuration Put Bucket Policy Delete Bucket Policy S3 Tables Admin (Full Access) Create Table Bucket Get Table Bucket List Table Buckets Delete Table Bucket Put Table Bucket Policy Get Table Bucket Policy Delete Table Bucket Policy Create Namespace Get Namespace List Namespaces Delete Namespace Create Table Get Table List Tables Delete Table Put Table Policy Get Table Policy Delete Table Policy Tag Resource List Tags Untag ResourceBucket Scope Apply selected permissions to specific buckets or all buckets All Buckets Specific Buckets Hold Ctrl/Cmd to select multiple bucketsOther Bucket Permissions Grants the permission and bucket pickers above cannot show. Uncheck a grant to remove it.Attached Policies GroupsAdd to group... User DetailsManage Access KeysAccess Keys for Create New KeyLeave blank to auto-generate.Access Key Secret Key Create Cancel") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/s3api/s3api_object_upload_acl.go b/weed/s3api/s3api_object_upload_acl.go index 4c02ed3c24..b9dd038fe7 100644 --- a/weed/s3api/s3api_object_upload_acl.go +++ b/weed/s3api/s3api_object_upload_acl.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/aws/aws-sdk-go/service/s3" + "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" @@ -156,8 +157,12 @@ func (s3a *S3ApiServer) preparePutObjectACL(r *http.Request, bucket string) (*ht // Setting an ACL during upload or copy also requires s3:PutObjectAcl. Use the // unified authorization path so bucket-policy allows and explicit denies // retain the same semantics as standalone ACL requests. - identity, authCode := s3a.iam.authRequest(r.Clone(r.Context()), s3_constants.ACTION_WRITE_ACP) + identity, authCode, _ := s3a.iam.authRequestWithAuthType(r.Clone(r.Context()), s3_constants.ACTION_WRITE_ACP) if authCode != s3err.ErrNone { + if authCode == s3err.ErrAccessDenied && identity != nil && identity.Name != s3_constants.AccountAnonymousId { + _, object := s3_constants.GetBucketAndObject(r) + glog.Infof("denied %s %q for %q: the request sets an ACL and failed the %s (s3:PutObjectAcl) check", r.Method, bucket+"/"+object, identity.Name, s3_constants.ACTION_WRITE_ACP) + } return r, authCode } if identity == nil || identity.Account == nil { diff --git a/weed/shell/command_s3_anonymous_set.go b/weed/shell/command_s3_anonymous_set.go index 0a2616cd0f..4468584f7e 100644 --- a/weed/shell/command_s3_anonymous_set.go +++ b/weed/shell/command_s3_anonymous_set.go @@ -31,7 +31,7 @@ func (c *commandS3AnonymousSet) Help() string { s3.anonymous.set -bucket -access Read,List s3.anonymous.set -bucket -access none - Supported actions: Read, Write, List, Tagging, Admin + Supported actions: Read, ReadAcp, Write, WriteAcp, List, Tagging, Admin Use "none" to remove all anonymous access for the bucket. This manages the special "anonymous" user's actions. It does not @@ -46,7 +46,7 @@ func (c *commandS3AnonymousSet) HasTag(CommandTag) bool { func (c *commandS3AnonymousSet) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error { f := flag.NewFlagSet(c.Name(), flag.ContinueOnError) bucket := f.String("bucket", "", "bucket name") - access := f.String("access", "", "comma-separated actions: Read,Write,List,Tagging,Admin or none") + access := f.String("access", "", "comma-separated actions: Read,ReadAcp,Write,WriteAcp,List,Tagging,Admin or none") if err := f.Parse(args); err != nil { return err } @@ -57,6 +57,16 @@ func (c *commandS3AnonymousSet) Do(args []string, commandEnv *CommandEnv, writer if *access == "" { return fmt.Errorf("-access is required") } + var granted []string + if strings.ToLower(strings.TrimSpace(*access)) != "none" { + var err error + if granted, err = canonicalActionList(*access); err != nil { + return err + } + if len(granted) == 0 { + return fmt.Errorf("invalid -access %q: use %s, or none", *access, supportedActionNames) + } + } return commandEnv.withIamClient(func(ctx context.Context, client iam_pb.SeaweedIdentityAccessManagementClient) error { // Get or create anonymous user @@ -74,27 +84,8 @@ func (c *commandS3AnonymousSet) Do(args []string, commandEnv *CommandEnv, writer } } - // Add new actions unless "none" - canonicalActions := map[string]string{ - "read": "Read", "write": "Write", "list": "List", - "tagging": "Tagging", "admin": "Admin", - } - if strings.ToLower(strings.TrimSpace(*access)) != "none" { - seen := make(map[string]struct{}) - for _, action := range strings.Split(*access, ",") { - action = strings.TrimSpace(action) - if action != "" { - canonical, ok := canonicalActions[strings.ToLower(action)] - if !ok { - return fmt.Errorf("invalid action %q: supported actions are Read, Write, List, Tagging, Admin", action) - } - if _, dup := seen[canonical]; dup { - continue - } - seen[canonical] = struct{}{} - kept = append(kept, canonical+":"+*bucket) - } - } + for _, action := range granted { + kept = append(kept, action+":"+*bucket) } identity.Actions = kept diff --git a/weed/shell/command_s3_bucket_access.go b/weed/shell/command_s3_bucket_access.go index f4f216e289..66e4b33e03 100644 --- a/weed/shell/command_s3_bucket_access.go +++ b/weed/shell/command_s3_bucket_access.go @@ -6,6 +6,7 @@ import ( "flag" "fmt" "io" + "slices" "sort" "strings" @@ -17,11 +18,35 @@ import ( // canonicalActions maps lowercased action names to their canonical form. var canonicalActions = map[string]string{ - "read": "Read", - "write": "Write", - "list": "List", - "tagging": "Tagging", - "admin": "Admin", + "read": "Read", + "readacp": "ReadAcp", + "write": "Write", + "writeacp": "WriteAcp", + "list": "List", + "tagging": "Tagging", + "admin": "Admin", +} + +const supportedActionNames = "Read, ReadAcp, Write, WriteAcp, List, Tagging, Admin" + +// canonicalActionList parses a comma-separated action list into canonical +// names, skipping empty entries and repeats. +func canonicalActionList(list string) ([]string, error) { + var actions []string + for _, a := range strings.Split(list, ",") { + a = strings.TrimSpace(a) + if a == "" { + continue + } + canonical, ok := canonicalActions[strings.ToLower(a)] + if !ok { + return nil, fmt.Errorf("invalid action %q: supported actions are %s", a, supportedActionNames) + } + if !slices.Contains(actions, canonical) { + actions = append(actions, canonical) + } + } + return actions, nil } func init() { @@ -45,14 +70,16 @@ func (c *commandS3BucketAccess) Help() string { # Grant anonymous read and list access s3.bucket.access -name -user anonymous -access Read,List - # Grant full anonymous access + # Grant anonymous read, write and list access s3.bucket.access -name -user anonymous -access Read,Write,List # Remove all access for a user on a bucket s3.bucket.access -name -user -access none Supported action names (comma-separated): - Read, Write, List, Tagging, Admin + Read, ReadAcp, Write, WriteAcp, List, Tagging, Admin + ReadAcp covers GET ?acl and WriteAcp covers PUT ?acl. An upload + that carries an ACL header such as x-amz-acl needs Write and WriteAcp. The user is auto-created if it does not exist. Actions are scoped to the specified bucket (stored as "Action:bucket" in the identity). @@ -68,7 +95,7 @@ func (c *commandS3BucketAccess) Do(args []string, commandEnv *CommandEnv, writer bucketCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError) bucketName := bucketCommand.String("name", "", "bucket name") userName := bucketCommand.String("user", "", "user name") - access := bucketCommand.String("access", "", "comma-separated actions: Read,Write,List,Tagging,Admin or none") + access := bucketCommand.String("access", "", "comma-separated actions: Read,ReadAcp,Write,WriteAcp,List,Tagging,Admin or none") if err = bucketCommand.Parse(args); err != nil { return err } @@ -82,16 +109,13 @@ func (c *commandS3BucketAccess) Do(args []string, commandEnv *CommandEnv, writer accessStr := strings.TrimSpace(*access) - // Validate and normalize actions to canonical casing if accessStr != "" && strings.ToLower(accessStr) != "none" { - var normalized []string - for _, a := range strings.Split(accessStr, ",") { - a = strings.TrimSpace(a) - canonical, ok := canonicalActions[strings.ToLower(a)] - if !ok { - return fmt.Errorf("invalid action %q: must be Read, Write, List, Tagging, Admin, or none", a) - } - normalized = append(normalized, canonical) + normalized, err := canonicalActionList(accessStr) + if err != nil { + return err + } + if len(normalized) == 0 { + return fmt.Errorf("invalid -access %q: use %s, or none", accessStr, supportedActionNames) } accessStr = strings.Join(normalized, ",") } diff --git a/weed/shell/command_s3_bucket_access_test.go b/weed/shell/command_s3_bucket_access_test.go index 2bcd4d3336..9c971da6ef 100644 --- a/weed/shell/command_s3_bucket_access_test.go +++ b/weed/shell/command_s3_bucket_access_test.go @@ -4,11 +4,54 @@ import ( "bytes" "slices" "sort" + "strings" "testing" "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" ) +func TestCanonicalActionsAcceptAcpActions(t *testing.T) { + names := strings.Split(supportedActionNames, ", ") + for _, action := range []string{ + s3_constants.ACTION_READ, s3_constants.ACTION_READ_ACP, + s3_constants.ACTION_WRITE, s3_constants.ACTION_WRITE_ACP, + s3_constants.ACTION_LIST, s3_constants.ACTION_TAGGING, s3_constants.ACTION_ADMIN, + } { + for _, input := range []string{action, strings.ToLower(action), strings.ToUpper(action)} { + if got := canonicalActions[strings.ToLower(input)]; got != action { + t.Errorf("canonicalActions[%q] = %q, want %q", input, got, action) + } + } + if !slices.Contains(names, action) { + t.Errorf("supportedActionNames %q does not list %s", supportedActionNames, action) + } + } + if len(names) != len(canonicalActions) { + t.Errorf("supportedActionNames lists %d actions, canonicalActions has %d", len(names), len(canonicalActions)) + } +} + +func TestCanonicalActionList(t *testing.T) { + got, err := canonicalActionList(" readacp, WriteAcp,,writeacp ,list") + if err != nil { + t.Fatalf("canonicalActionList: %v", err) + } + if want := []string{"ReadAcp", "WriteAcp", "List"}; !slices.Equal(got, want) { + t.Errorf("got %v, want %v", got, want) + } + if _, err := canonicalActionList("Read,PutObjectAcl"); err == nil { + t.Error("expected an error for an unsupported action") + } +} + +func TestAnonymousSetRejectsEmptyAccess(t *testing.T) { + err := (&commandS3AnonymousSet{}).Do([]string{"-bucket", "b", "-access", " , ,"}, nil, &bytes.Buffer{}) + if err == nil || !strings.Contains(err.Error(), "invalid -access") { + t.Errorf("got %v, want an invalid -access error", err) + } +} + func TestUpdateBucketActions_SetActions(t *testing.T) { identity := &iam_pb.Identity{ Name: "anonymous", @@ -39,6 +82,24 @@ func TestUpdateBucketActions_ReplaceActions(t *testing.T) { } } +func TestUpdateBucketActions_AcpActions(t *testing.T) { + identity := &iam_pb.Identity{ + Name: "testuser", + Actions: []string{"Read:my-bucket", "Write:other-bucket"}, + } + + normalized, err := canonicalActionList("read,readacp,WRITEACP") + if err != nil { + t.Fatalf("canonicalActionList: %v", err) + } + updateBucketActions(identity, "my-bucket", strings.Join(normalized, ",")) + + expected := []string{"Write:other-bucket", "Read:my-bucket", "ReadAcp:my-bucket", "WriteAcp:my-bucket"} + if !slices.Equal(identity.Actions, expected) { + t.Errorf("got %v, want %v", identity.Actions, expected) + } +} + func TestUpdateBucketActions_None(t *testing.T) { identity := &iam_pb.Identity{ Name: "anonymous", diff --git a/weed/shell/command_s3_circuitbreaker.go b/weed/shell/command_s3_circuitbreaker.go index 772cacc789..e4d7cbef14 100644 --- a/weed/shell/command_s3_circuitbreaker.go +++ b/weed/shell/command_s3_circuitbreaker.go @@ -65,7 +65,7 @@ func (c *commandS3CircuitBreaker) Do(args []string, commandEnv *CommandEnv, writ buckets := s3CircuitBreakerCommand.String("buckets", "", "the bucket name(s) to configure, eg: -buckets x,y,z") global := s3CircuitBreakerCommand.Bool("global", false, "configure global circuit breaker") - actions := s3CircuitBreakerCommand.String("actions", "", "comma separated actions names: Read,Write,List,Tagging,Admin") + actions := s3CircuitBreakerCommand.String("actions", "", "comma separated actions names: "+strings.Join(s3_constants.AllowedActions, ",")) limitType := s3CircuitBreakerCommand.String("type", "", "'Count' or 'MB'; Count represents the number of simultaneous requests, and MB represents the content size of all simultaneous requests") values := s3CircuitBreakerCommand.String("values", "", "comma separated values") diff --git a/weed/shell/command_s3_configure.go b/weed/shell/command_s3_configure.go index f321e1ba71..eb6b8a5cc2 100644 --- a/weed/shell/command_s3_configure.go +++ b/weed/shell/command_s3_configure.go @@ -34,6 +34,13 @@ func (c *commandS3Configure) Help() string { # create a new identity with account information s3.configure -user=username -actions=Read,Write,List,Tagging -buckets=bucket-name -policies=policy1,policy2 -access_key=key -secret_key=secret -account_id=id -account_display_name=name -account_email=email@example.com -apply + + # allow uploads that send an ACL header such as x-amz-acl + s3.configure -user=username -actions=Write,WriteAcp -buckets=bucket-name -apply + + Common action names: Read, ReadAcp, Write, WriteAcp, List, Tagging, Admin + ReadAcp covers GET ?acl and WriteAcp covers PUT ?acl. An upload or copy + that carries an ACL header such as x-amz-acl needs both Write and WriteAcp. ` } @@ -44,7 +51,7 @@ func (c *commandS3Configure) HasTag(tag CommandTag) bool { func (c *commandS3Configure) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) { s3ConfigureCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError) - actions := s3ConfigureCommand.String("actions", "", "comma separated actions names: Read,Write,List,Tagging,Admin") + actions := s3ConfigureCommand.String("actions", "", "comma separated actions names: Read,ReadAcp,Write,WriteAcp,List,Tagging,Admin") user := s3ConfigureCommand.String("user", "", "user name") buckets := s3ConfigureCommand.String("buckets", "", "bucket name") accessKey := s3ConfigureCommand.String("access_key", "", "specify the access key") diff --git a/weed/shell/command_s3_serviceaccount_create.go b/weed/shell/command_s3_serviceaccount_create.go index ca6b076c76..fc3c5b972d 100644 --- a/weed/shell/command_s3_serviceaccount_create.go +++ b/weed/shell/command_s3_serviceaccount_create.go @@ -7,7 +7,6 @@ import ( "flag" "fmt" "io" - "strings" "time" "github.com/seaweedfs/seaweedfs/weed/iam" @@ -84,26 +83,12 @@ func (c *commandS3ServiceAccountCreate) Do(args []string, commandEnv *CommandEnv CreatedAt: time.Now().Unix(), } - validActions := map[string]string{ - "read": "Read", "write": "Write", "list": "List", - "tagging": "Tagging", "admin": "Admin", - } if *actions != "" { - seen := make(map[string]struct{}) - for _, a := range strings.Split(*actions, ",") { - a = strings.TrimSpace(a) - if a != "" { - canonical, ok := validActions[strings.ToLower(a)] - if !ok { - return fmt.Errorf("invalid action %q: supported actions are Read, Write, List, Tagging, Admin", a) - } - if _, dup := seen[canonical]; dup { - continue - } - seen[canonical] = struct{}{} - sa.Actions = append(sa.Actions, canonical) - } + parsed, err := canonicalActionList(*actions) + if err != nil { + return err } + sa.Actions = parsed } if *expiry < 0 {
Leave blank to auto-generate.