* shell: add s3.bucket.access command for anonymous access policy (#7738) Add a new weed shell command to view or change the anonymous access policy of an S3 bucket without external tools. Usage: s3.bucket.access -name <bucket> -access read,list s3.bucket.access -name <bucket> -access none Supported permissions: read, write, list. The command writes a standard bucket policy with Principal "*" and warns if no anonymous IAM identity exists. * shell: fix anonymous identity hint in s3.bucket.access warning The anonymous identity doesn't need IAM actions — the bucket policy controls what anonymous users can do. * shell: only warn about anonymous identity when write access is set Read and list operations use AuthWithPublicRead which evaluates bucket policies directly without requiring the anonymous identity. Only write operations go through the normal auth flow that needs it. * shell: rewrite s3.bucket.access to use IAM actions instead of bucket policies Replace the bucket policy approach with direct IAM identity actions, matching the s3.configure pattern. The user is auto-created if it does not exist. Usage: s3.bucket.access -name <bucket> -user anonymous -access Read,List s3.bucket.access -name <bucket> -user anonymous -access none s3.bucket.access -name <bucket> -user anonymous Actions are stored as "Action:bucket" on the identity, same as s3.configure -actions=Read -buckets=my-bucket. * shell: return flag parse errors instead of swallowing them * shell: normalize action names case-insensitively in s3.bucket.access Accept actions in any case (read, READ, Read) and normalize to canonical form (Read, Write, List, etc.) before storing. This matches the case-insensitive handling of "none" and avoids confusing rejections.
120 lines
2.9 KiB
Go
120 lines
2.9 KiB
Go
package shell
|
|
|
|
import (
|
|
"bytes"
|
|
"slices"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
|
)
|
|
|
|
func TestUpdateBucketActions_SetActions(t *testing.T) {
|
|
identity := &iam_pb.Identity{
|
|
Name: "anonymous",
|
|
Actions: []string{},
|
|
}
|
|
|
|
updateBucketActions(identity, "my-bucket", "Read,List")
|
|
|
|
expected := []string{"Read:my-bucket", "List:my-bucket"}
|
|
sort.Strings(identity.Actions)
|
|
sort.Strings(expected)
|
|
if !slices.Equal(identity.Actions, expected) {
|
|
t.Errorf("got %v, want %v", identity.Actions, expected)
|
|
}
|
|
}
|
|
|
|
func TestUpdateBucketActions_ReplaceActions(t *testing.T) {
|
|
identity := &iam_pb.Identity{
|
|
Name: "anonymous",
|
|
Actions: []string{"Read:my-bucket", "List:my-bucket"},
|
|
}
|
|
|
|
updateBucketActions(identity, "my-bucket", "Write")
|
|
|
|
expected := []string{"Write: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",
|
|
Actions: []string{"Read:my-bucket", "List:my-bucket"},
|
|
}
|
|
|
|
updateBucketActions(identity, "my-bucket", "none")
|
|
|
|
if len(identity.Actions) != 0 {
|
|
t.Errorf("expected empty actions, got %v", identity.Actions)
|
|
}
|
|
}
|
|
|
|
func TestUpdateBucketActions_PreservesOtherBuckets(t *testing.T) {
|
|
identity := &iam_pb.Identity{
|
|
Name: "testuser",
|
|
Actions: []string{"Read:bucket-a", "Write:bucket-b", "List:bucket-a"},
|
|
}
|
|
|
|
updateBucketActions(identity, "bucket-a", "Write")
|
|
|
|
expected := []string{"Write:bucket-b", "Write:bucket-a"}
|
|
sort.Strings(identity.Actions)
|
|
sort.Strings(expected)
|
|
if !slices.Equal(identity.Actions, expected) {
|
|
t.Errorf("got %v, want %v", identity.Actions, expected)
|
|
}
|
|
}
|
|
|
|
func TestUpdateBucketActions_PreservesGlobalActions(t *testing.T) {
|
|
identity := &iam_pb.Identity{
|
|
Name: "testuser",
|
|
Actions: []string{"Admin", "Read:my-bucket"},
|
|
}
|
|
|
|
updateBucketActions(identity, "my-bucket", "none")
|
|
|
|
expected := []string{"Admin"}
|
|
if !slices.Equal(identity.Actions, expected) {
|
|
t.Errorf("got %v, want %v", identity.Actions, expected)
|
|
}
|
|
}
|
|
|
|
func TestDisplayBucketAccess(t *testing.T) {
|
|
identity := &iam_pb.Identity{
|
|
Name: "anonymous",
|
|
Actions: []string{"Read:my-bucket", "List:my-bucket", "Write:other-bucket"},
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err := displayBucketAccess(&buf, "my-bucket", "anonymous", identity)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
output := buf.String()
|
|
if !bytes.Contains([]byte(output), []byte("List,Read")) {
|
|
t.Errorf("expected 'List,Read' in output, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestDisplayBucketAccess_None(t *testing.T) {
|
|
identity := &iam_pb.Identity{
|
|
Name: "anonymous",
|
|
Actions: []string{"Write:other-bucket"},
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err := displayBucketAccess(&buf, "my-bucket", "anonymous", identity)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
output := buf.String()
|
|
if !bytes.Contains([]byte(output), []byte("Access: none")) {
|
|
t.Errorf("expected 'Access: none' in output, got: %s", output)
|
|
}
|
|
}
|