| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545 |
- package s3api
- import (
- "os"
- "reflect"
- "testing"
- "github.com/seaweedfs/seaweedfs/weed/credential"
- . "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
- "github.com/stretchr/testify/assert"
- "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
- jsonpb "google.golang.org/protobuf/encoding/protojson"
- )
- func TestIdentityListFileFormat(t *testing.T) {
- s3ApiConfiguration := &iam_pb.S3ApiConfiguration{}
- identity1 := &iam_pb.Identity{
- Name: "some_name",
- Credentials: []*iam_pb.Credential{
- {
- AccessKey: "some_access_key1",
- SecretKey: "some_secret_key2",
- },
- },
- Actions: []string{
- ACTION_ADMIN,
- ACTION_READ,
- ACTION_WRITE,
- },
- }
- identity2 := &iam_pb.Identity{
- Name: "some_read_only_user",
- Credentials: []*iam_pb.Credential{
- {
- AccessKey: "some_access_key1",
- SecretKey: "some_secret_key1",
- },
- },
- Actions: []string{
- ACTION_READ,
- },
- }
- identity3 := &iam_pb.Identity{
- Name: "some_normal_user",
- Credentials: []*iam_pb.Credential{
- {
- AccessKey: "some_access_key2",
- SecretKey: "some_secret_key2",
- },
- },
- Actions: []string{
- ACTION_READ,
- ACTION_WRITE,
- },
- }
- s3ApiConfiguration.Identities = append(s3ApiConfiguration.Identities, identity1)
- s3ApiConfiguration.Identities = append(s3ApiConfiguration.Identities, identity2)
- s3ApiConfiguration.Identities = append(s3ApiConfiguration.Identities, identity3)
- m := jsonpb.MarshalOptions{
- EmitUnpopulated: true,
- Indent: " ",
- }
- text, _ := m.Marshal(s3ApiConfiguration)
- println(string(text))
- }
- func TestCanDo(t *testing.T) {
- ident1 := &Identity{
- Name: "anything",
- Actions: []Action{
- "Write:bucket1/a/b/c/*",
- "Write:bucket1/a/b/other",
- },
- }
- // object specific
- assert.Equal(t, true, ident1.canDo(ACTION_WRITE, "bucket1", "/a/b/c/d.txt"))
- assert.Equal(t, true, ident1.canDo(ACTION_WRITE, "bucket1", "/a/b/c/d/e.txt"))
- assert.Equal(t, false, ident1.canDo(ACTION_DELETE_BUCKET, "bucket1", ""))
- assert.Equal(t, false, ident1.canDo(ACTION_WRITE, "bucket1", "/a/b/other/some"), "action without *")
- assert.Equal(t, false, ident1.canDo(ACTION_WRITE, "bucket1", "/a/b/*"), "action on parent directory")
- // bucket specific
- ident2 := &Identity{
- Name: "anything",
- Actions: []Action{
- "Read:bucket1",
- "Write:bucket1/*",
- "WriteAcp:bucket1",
- },
- }
- assert.Equal(t, true, ident2.canDo(ACTION_READ, "bucket1", "/a/b/c/d.txt"))
- assert.Equal(t, true, ident2.canDo(ACTION_WRITE, "bucket1", "/a/b/c/d.txt"))
- assert.Equal(t, true, ident2.canDo(ACTION_WRITE_ACP, "bucket1", ""))
- assert.Equal(t, false, ident2.canDo(ACTION_READ_ACP, "bucket1", ""))
- assert.Equal(t, false, ident2.canDo(ACTION_LIST, "bucket1", "/a/b/c/d.txt"))
- // across buckets
- ident3 := &Identity{
- Name: "anything",
- Actions: []Action{
- "Read",
- "Write",
- },
- }
- assert.Equal(t, true, ident3.canDo(ACTION_READ, "bucket1", "/a/b/c/d.txt"))
- assert.Equal(t, true, ident3.canDo(ACTION_WRITE, "bucket1", "/a/b/c/d.txt"))
- assert.Equal(t, false, ident3.canDo(ACTION_LIST, "bucket1", "/a/b/other/some"))
- assert.Equal(t, false, ident3.canDo(ACTION_WRITE_ACP, "bucket1", ""))
- // partial buckets
- ident4 := &Identity{
- Name: "anything",
- Actions: []Action{
- "Read:special_*",
- "ReadAcp:special_*",
- },
- }
- assert.Equal(t, true, ident4.canDo(ACTION_READ, "special_bucket", "/a/b/c/d.txt"))
- assert.Equal(t, true, ident4.canDo(ACTION_READ_ACP, "special_bucket", ""))
- assert.Equal(t, false, ident4.canDo(ACTION_READ, "bucket1", "/a/b/c/d.txt"))
- // admin buckets
- ident5 := &Identity{
- Name: "anything",
- Actions: []Action{
- "Admin:special_*",
- },
- }
- assert.Equal(t, true, ident5.canDo(ACTION_READ, "special_bucket", "/a/b/c/d.txt"))
- assert.Equal(t, true, ident5.canDo(ACTION_READ_ACP, "special_bucket", ""))
- assert.Equal(t, true, ident5.canDo(ACTION_WRITE, "special_bucket", "/a/b/c/d.txt"))
- assert.Equal(t, true, ident5.canDo(ACTION_WRITE_ACP, "special_bucket", ""))
- // anonymous buckets
- ident6 := &Identity{
- Name: "anonymous",
- Actions: []Action{
- "Read",
- },
- }
- assert.Equal(t, true, ident6.canDo(ACTION_READ, "anything_bucket", "/a/b/c/d.txt"))
- //test deleteBucket operation
- ident7 := &Identity{
- Name: "anything",
- Actions: []Action{
- "DeleteBucket:bucket1",
- },
- }
- assert.Equal(t, true, ident7.canDo(ACTION_DELETE_BUCKET, "bucket1", ""))
- }
- type LoadS3ApiConfigurationTestCase struct {
- pbAccount *iam_pb.Account
- pbIdent *iam_pb.Identity
- expectIdent *Identity
- }
- func TestLoadS3ApiConfiguration(t *testing.T) {
- specifiedAccount := Account{
- Id: "specifiedAccountID",
- DisplayName: "specifiedAccountName",
- EmailAddress: "specifiedAccounEmail@example.com",
- }
- pbSpecifiedAccount := iam_pb.Account{
- Id: "specifiedAccountID",
- DisplayName: "specifiedAccountName",
- EmailAddress: "specifiedAccounEmail@example.com",
- }
- testCases := map[string]*LoadS3ApiConfigurationTestCase{
- "notSpecifyAccountId": {
- pbIdent: &iam_pb.Identity{
- Name: "notSpecifyAccountId",
- Actions: []string{
- "Read",
- "Write",
- },
- Credentials: []*iam_pb.Credential{
- {
- AccessKey: "some_access_key1",
- SecretKey: "some_secret_key2",
- },
- },
- },
- expectIdent: &Identity{
- Name: "notSpecifyAccountId",
- Account: &AccountAdmin,
- PrincipalArn: "arn:seaweed:iam::user/notSpecifyAccountId",
- Actions: []Action{
- "Read",
- "Write",
- },
- Credentials: []*Credential{
- {
- AccessKey: "some_access_key1",
- SecretKey: "some_secret_key2",
- },
- },
- },
- },
- "specifiedAccountID": {
- pbAccount: &pbSpecifiedAccount,
- pbIdent: &iam_pb.Identity{
- Name: "specifiedAccountID",
- Account: &pbSpecifiedAccount,
- Actions: []string{
- "Read",
- "Write",
- },
- },
- expectIdent: &Identity{
- Name: "specifiedAccountID",
- Account: &specifiedAccount,
- PrincipalArn: "arn:seaweed:iam::user/specifiedAccountID",
- Actions: []Action{
- "Read",
- "Write",
- },
- },
- },
- "anonymous": {
- pbIdent: &iam_pb.Identity{
- Name: "anonymous",
- Actions: []string{
- "Read",
- "Write",
- },
- },
- expectIdent: &Identity{
- Name: "anonymous",
- Account: &AccountAnonymous,
- PrincipalArn: "arn:seaweed:iam::user/anonymous",
- Actions: []Action{
- "Read",
- "Write",
- },
- },
- },
- }
- config := &iam_pb.S3ApiConfiguration{
- Identities: make([]*iam_pb.Identity, 0),
- }
- for _, v := range testCases {
- config.Identities = append(config.Identities, v.pbIdent)
- if v.pbAccount != nil {
- config.Accounts = append(config.Accounts, v.pbAccount)
- }
- }
- iam := IdentityAccessManagement{}
- err := iam.loadS3ApiConfiguration(config)
- if err != nil {
- return
- }
- for _, ident := range iam.identities {
- tc := testCases[ident.Name]
- if !reflect.DeepEqual(ident, tc.expectIdent) {
- t.Errorf("not expect for ident name %s", ident.Name)
- }
- }
- }
- func TestNewIdentityAccessManagementWithStoreEnvVars(t *testing.T) {
- // Save original environment
- originalAccessKeyId := os.Getenv("AWS_ACCESS_KEY_ID")
- originalSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
- // Clean up after test
- defer func() {
- if originalAccessKeyId != "" {
- os.Setenv("AWS_ACCESS_KEY_ID", originalAccessKeyId)
- } else {
- os.Unsetenv("AWS_ACCESS_KEY_ID")
- }
- if originalSecretAccessKey != "" {
- os.Setenv("AWS_SECRET_ACCESS_KEY", originalSecretAccessKey)
- } else {
- os.Unsetenv("AWS_SECRET_ACCESS_KEY")
- }
- }()
- tests := []struct {
- name string
- accessKeyId string
- secretAccessKey string
- expectEnvIdentity bool
- expectedName string
- description string
- }{
- {
- name: "Environment variables used as fallback",
- accessKeyId: "AKIA1234567890ABCDEF",
- secretAccessKey: "secret123456789012345678901234567890abcdef12",
- expectEnvIdentity: true,
- expectedName: "admin-AKIA1234",
- description: "When no config file and no filer config, environment variables should be used",
- },
- {
- name: "Short access key fallback",
- accessKeyId: "SHORT",
- secretAccessKey: "secret123456789012345678901234567890abcdef12",
- expectEnvIdentity: true,
- expectedName: "admin-SHORT",
- description: "Short access keys should work correctly as fallback",
- },
- {
- name: "No env vars means no identities",
- accessKeyId: "",
- secretAccessKey: "",
- expectEnvIdentity: false,
- expectedName: "",
- description: "When no env vars and no config, should have no identities",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- // Set up environment variables
- if tt.accessKeyId != "" {
- os.Setenv("AWS_ACCESS_KEY_ID", tt.accessKeyId)
- } else {
- os.Unsetenv("AWS_ACCESS_KEY_ID")
- }
- if tt.secretAccessKey != "" {
- os.Setenv("AWS_SECRET_ACCESS_KEY", tt.secretAccessKey)
- } else {
- os.Unsetenv("AWS_SECRET_ACCESS_KEY")
- }
- // Create IAM instance with memory store for testing (no config file)
- option := &S3ApiServerOption{
- Config: "", // No config file - this should trigger environment variable fallback
- }
- iam := NewIdentityAccessManagementWithStore(option, string(credential.StoreTypeMemory))
- if tt.expectEnvIdentity {
- // Should have exactly one identity from environment variables
- assert.Len(t, iam.identities, 1, "Should have exactly one identity from environment variables")
- identity := iam.identities[0]
- assert.Equal(t, tt.expectedName, identity.Name, "Identity name should match expected")
- assert.Len(t, identity.Credentials, 1, "Should have one credential")
- assert.Equal(t, tt.accessKeyId, identity.Credentials[0].AccessKey, "Access key should match environment variable")
- assert.Equal(t, tt.secretAccessKey, identity.Credentials[0].SecretKey, "Secret key should match environment variable")
- assert.Contains(t, identity.Actions, Action(ACTION_ADMIN), "Should have admin action")
- } else {
- // When no env vars, should have no identities (since no config file)
- assert.Len(t, iam.identities, 0, "Should have no identities when no env vars and no config file")
- }
- })
- }
- }
- // TestBucketLevelListPermissions tests that bucket-level List permissions work correctly
- // This test validates the fix for issue #7066
- func TestBucketLevelListPermissions(t *testing.T) {
- // Test the functionality that was broken in issue #7066
- t.Run("Bucket Wildcard Permissions", func(t *testing.T) {
- // Create identity with bucket-level List permission using wildcards
- identity := &Identity{
- Name: "bucket-user",
- Actions: []Action{
- "List:mybucket*",
- "Read:mybucket*",
- "ReadAcp:mybucket*",
- "Write:mybucket*",
- "WriteAcp:mybucket*",
- "Tagging:mybucket*",
- },
- }
- // Test cases for bucket-level wildcard permissions
- testCases := []struct {
- name string
- action Action
- bucket string
- object string
- shouldAllow bool
- description string
- }{
- {
- name: "exact bucket match",
- action: "List",
- bucket: "mybucket",
- object: "",
- shouldAllow: true,
- description: "Should allow access to exact bucket name",
- },
- {
- name: "bucket with suffix",
- action: "List",
- bucket: "mybucket-prod",
- object: "",
- shouldAllow: true,
- description: "Should allow access to bucket with matching prefix",
- },
- {
- name: "bucket with numbers",
- action: "List",
- bucket: "mybucket123",
- object: "",
- shouldAllow: true,
- description: "Should allow access to bucket with numbers",
- },
- {
- name: "different bucket",
- action: "List",
- bucket: "otherbucket",
- object: "",
- shouldAllow: false,
- description: "Should deny access to bucket with different prefix",
- },
- {
- name: "partial match",
- action: "List",
- bucket: "notmybucket",
- object: "",
- shouldAllow: false,
- description: "Should deny access to bucket that contains but doesn't start with the prefix",
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- result := identity.canDo(tc.action, tc.bucket, tc.object)
- assert.Equal(t, tc.shouldAllow, result, tc.description)
- })
- }
- })
- t.Run("Global List Permission", func(t *testing.T) {
- // Create identity with global List permission
- identity := &Identity{
- Name: "global-user",
- Actions: []Action{
- "List",
- },
- }
- // Should allow access to any bucket
- testCases := []string{"anybucket", "mybucket", "test-bucket", "prod-data"}
- for _, bucket := range testCases {
- result := identity.canDo("List", bucket, "")
- assert.True(t, result, "Global List permission should allow access to bucket %s", bucket)
- }
- })
- t.Run("No Wildcard Exact Match", func(t *testing.T) {
- // Create identity with exact bucket permission (no wildcard)
- identity := &Identity{
- Name: "exact-user",
- Actions: []Action{
- "List:specificbucket",
- },
- }
- // Should only allow access to the exact bucket
- assert.True(t, identity.canDo("List", "specificbucket", ""), "Should allow access to exact bucket")
- assert.False(t, identity.canDo("List", "specificbucket-test", ""), "Should deny access to bucket with suffix")
- assert.False(t, identity.canDo("List", "otherbucket", ""), "Should deny access to different bucket")
- })
- t.Log("This test validates the fix for issue #7066")
- t.Log("Bucket-level List permissions like 'List:bucket*' work correctly")
- t.Log("ListBucketsHandler now uses consistent authentication flow")
- }
- // TestListBucketsAuthRequest tests that authRequest works correctly for ListBuckets operations
- // This test validates that the fix for the regression identified in PR #7067 works correctly
- func TestListBucketsAuthRequest(t *testing.T) {
- t.Run("ListBuckets special case handling", func(t *testing.T) {
- // Create identity with bucket-specific permissions (no global List permission)
- identity := &Identity{
- Name: "bucket-user",
- Account: &AccountAdmin,
- Actions: []Action{
- Action("List:mybucket*"),
- Action("Read:mybucket*"),
- },
- }
- // Test 1: ListBuckets operation should succeed (bucket = "")
- // This would have failed before the fix because canDo("List", "", "") would return false
- // After the fix, it bypasses the canDo check for ListBuckets operations
- // Simulate what happens in authRequest for ListBuckets:
- // action = ACTION_LIST, bucket = "", object = ""
- // Before fix: identity.canDo(ACTION_LIST, "", "") would fail
- // After fix: the canDo check should be bypassed
- // Test the individual canDo method to show it would fail without the special case
- result := identity.canDo(Action(ACTION_LIST), "", "")
- assert.False(t, result, "canDo should return false for empty bucket with bucket-specific permissions")
- // Test with a specific bucket that matches the permission
- result2 := identity.canDo(Action(ACTION_LIST), "mybucket", "")
- assert.True(t, result2, "canDo should return true for matching bucket")
- // Test with a specific bucket that doesn't match
- result3 := identity.canDo(Action(ACTION_LIST), "otherbucket", "")
- assert.False(t, result3, "canDo should return false for non-matching bucket")
- })
- t.Run("Object listing maintains permission enforcement", func(t *testing.T) {
- // Create identity with bucket-specific permissions
- identity := &Identity{
- Name: "bucket-user",
- Account: &AccountAdmin,
- Actions: []Action{
- Action("List:mybucket*"),
- },
- }
- // For object listing operations, the normal permission checks should still apply
- // These operations have a specific bucket in the URL
- // Should succeed for allowed bucket
- result1 := identity.canDo(Action(ACTION_LIST), "mybucket", "prefix/")
- assert.True(t, result1, "Should allow listing objects in permitted bucket")
- result2 := identity.canDo(Action(ACTION_LIST), "mybucket-prod", "")
- assert.True(t, result2, "Should allow listing objects in wildcard-matched bucket")
- // Should fail for disallowed bucket
- result3 := identity.canDo(Action(ACTION_LIST), "otherbucket", "")
- assert.False(t, result3, "Should deny listing objects in non-permitted bucket")
- })
- t.Log("This test validates the fix for the regression identified in PR #7067")
- t.Log("ListBuckets operation bypasses global permission check when bucket is empty")
- t.Log("Object listing still properly enforces bucket-level permissions")
- }
|