// Copyright (c) 2015-2021 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 cmd import ( "bytes" "context" "fmt" "io" "os" "reflect" "strings" "testing" "time" "github.com/klauspost/compress/zip" "github.com/minio/madmin-go/v3" minio "github.com/minio/minio-go/v7" cr "github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/set" ldap "github.com/minio/pkg/v3/ldap" "golang.org/x/exp/slices" ) func runAllIAMSTSTests(suite *TestSuiteIAM, c *check) { suite.SetUpSuite(c) // The STS for root test needs to be the first one after setup. suite.TestSTSForRoot(c) suite.TestSTS(c) suite.TestSTSWithDenyDeleteVersion(c) suite.TestSTSWithTags(c) suite.TestSTSServiceAccountsWithUsername(c) suite.TestSTSWithGroupPolicy(c) suite.TearDownSuite(c) } func TestIAMInternalIDPSTSServerSuite(t *testing.T) { baseTestCases := []TestSuiteCommon{ // Init and run test on ErasureSD backend with signature v4. {serverType: "ErasureSD", signer: signerV4}, // Init and run test on ErasureSD backend, with tls enabled. {serverType: "ErasureSD", signer: signerV4, secure: true}, // Init and run test on Erasure backend. {serverType: "Erasure", signer: signerV4}, // Init and run test on ErasureSet backend. {serverType: "ErasureSet", signer: signerV4}, } testCases := []*TestSuiteIAM{} for _, bt := range baseTestCases { testCases = append(testCases, newTestSuiteIAM(bt, false), newTestSuiteIAM(bt, true), ) } for i, testCase := range testCases { etcdStr := "" if testCase.withEtcdBackend { etcdStr = " (with etcd backend)" } t.Run( fmt.Sprintf("Test: %d, ServerType: %s%s", i+1, testCase.serverType, etcdStr), func(t *testing.T) { runAllIAMSTSTests(testCase, &check{t, testCase.serverType}) }, ) } } func (s *TestSuiteIAM) TestSTSServiceAccountsWithUsername(c *check) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() bucket := "dillon-bucket" err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Create policy policy := "mypolicy-username" policyBytes := []byte(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:*" ], "Resource": [ "arn:aws:s3:::${aws:username}-*" ] } ] }`) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } if err = s.adm.AddUser(ctx, "dillon", "dillon-123"); err != nil { c.Fatalf("policy add error: %v", err) } err = s.adm.SetPolicy(ctx, policy, "dillon", false) if err != nil { c.Fatalf("Unable to set policy: %v", err) } assumeRole := cr.STSAssumeRole{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, Options: cr.STSAssumeRoleOptions{ AccessKey: "dillon", SecretKey: "dillon-123", Location: "", }, } value, err := assumeRole.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } // Check that the LDAP sts cred is actually working. minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Create an madmin client with user creds userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, }) if err != nil { c.Fatalf("Err creating user admin client: %v", err) } userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport) // Create svc acc cr := c.mustCreateSvcAccount(ctx, value.AccessKeyID, userAdmClient) svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "") // 1. Check S3 access for service account ListObjects() c.mustListObjects(ctx, svcClient, bucket) // 2. Check S3 access for upload c.mustUpload(ctx, svcClient, bucket) // 3. Check S3 access for download c.mustDownload(ctx, svcClient, bucket) } func (s *TestSuiteIAM) TestSTSWithDenyDeleteVersion(c *check) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{ObjectLocking: true}) if err != nil { c.Fatalf("bucket creat error: %v", err) } // Create policy, user and associate policy policy := "mypolicy" policyBytes := []byte(fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Sid": "ObjectActionsRW", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:PutObjectTagging", "s3:AbortMultipartUpload", "s3:DeleteObject", "s3:GetObject", "s3:GetObjectTagging", "s3:GetObjectVersion", "s3:ListMultipartUploadParts" ], "Resource": [ "arn:aws:s3:::%s/*" ] }, { "Sid": "DenyDeleteVersionAction", "Effect": "Deny", "Action": [ "s3:DeleteObjectVersion" ], "Resource": [ "arn:aws:s3:::%s/*" ] } ] } `, bucket, bucket)) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } accessKey, secretKey := mustGenerateCredentials(c) err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled) if err != nil { c.Fatalf("Unable to set user: %v", err) } err = s.adm.SetPolicy(ctx, policy, accessKey, false) if err != nil { c.Fatalf("Unable to set policy: %v", err) } // confirm that the user is able to access the bucket uClient := s.getUserClient(c, accessKey, secretKey, "") versions := c.mustUploadReturnVersions(ctx, uClient, bucket) c.mustNotDelete(ctx, uClient, bucket, versions[0]) assumeRole := cr.STSAssumeRole{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, Options: cr.STSAssumeRoleOptions{ AccessKey: accessKey, SecretKey: secretKey, Location: "", }, } value, err := assumeRole.Retrieve() if err != nil { c.Fatalf("err calling assumeRole: %v", err) } minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } versions = c.mustUploadReturnVersions(ctx, minioClient, bucket) c.mustNotDelete(ctx, minioClient, bucket, versions[0]) } func (s *TestSuiteIAM) TestSTSWithTags(c *check) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() bucket := getRandomBucketName() object := getRandomObjectName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket creat error: %v", err) } // Create policy, user and associate policy policy := "mypolicy" policyBytes := []byte(fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::%s/*", "Condition": { "StringEquals": {"s3:ExistingObjectTag/security": "public" } } }, { "Effect": "Allow", "Action": "s3:DeleteObjectTagging", "Resource": "arn:aws:s3:::%s/*", "Condition": { "StringEquals": {"s3:ExistingObjectTag/security": "public" } } }, { "Effect": "Allow", "Action": "s3:DeleteObject", "Resource": "arn:aws:s3:::%s/*" }, { "Effect": "Allow", "Action": [ "s3:PutObject" ], "Resource": [ "arn:aws:s3:::%s/*" ], "Condition": { "ForAllValues:StringLike": { "s3:RequestObjectTagKeys": [ "security", "virus" ] } } } ] }`, bucket, bucket, bucket, bucket)) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } accessKey, secretKey := mustGenerateCredentials(c) err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled) if err != nil { c.Fatalf("Unable to set user: %v", err) } err = s.adm.SetPolicy(ctx, policy, accessKey, false) if err != nil { c.Fatalf("Unable to set policy: %v", err) } // confirm that the user is able to access the bucket uClient := s.getUserClient(c, accessKey, secretKey, "") c.mustPutObjectWithTags(ctx, uClient, bucket, object) c.mustGetObject(ctx, uClient, bucket, object) assumeRole := cr.STSAssumeRole{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, Options: cr.STSAssumeRoleOptions{ AccessKey: accessKey, SecretKey: secretKey, Location: "", }, } value, err := assumeRole.Retrieve() if err != nil { c.Fatalf("err calling assumeRole: %v", err) } minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate sts creds can access the object c.mustPutObjectWithTags(ctx, minioClient, bucket, object) c.mustGetObject(ctx, minioClient, bucket, object) c.mustHeadObject(ctx, minioClient, bucket, object, 2) // Validate that the client can remove objects if err = minioClient.RemoveObjectTagging(ctx, bucket, object, minio.RemoveObjectTaggingOptions{}); err != nil { c.Fatalf("user is unable to delete the object tags: %v", err) } if err = minioClient.RemoveObject(ctx, bucket, object, minio.RemoveObjectOptions{}); err != nil { c.Fatalf("user is unable to delete the object: %v", err) } } func (s *TestSuiteIAM) TestSTS(c *check) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket creat error: %v", err) } // Create policy, user and associate policy policy := "mypolicy" policyBytes := []byte(fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::%s/*" ] } ] }`, bucket)) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } accessKey, secretKey := mustGenerateCredentials(c) err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled) if err != nil { c.Fatalf("Unable to set user: %v", err) } err = s.adm.SetPolicy(ctx, policy, accessKey, false) if err != nil { c.Fatalf("Unable to set policy: %v", err) } // confirm that the user is able to access the bucket uClient := s.getUserClient(c, accessKey, secretKey, "") c.mustListObjects(ctx, uClient, bucket) assumeRole := cr.STSAssumeRole{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, Options: cr.STSAssumeRoleOptions{ AccessKey: accessKey, SecretKey: secretKey, Location: "", }, } value, err := assumeRole.Retrieve() if err != nil { c.Fatalf("err calling assumeRole: %v", err) } minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Validate that the client cannot remove any objects err = minioClient.RemoveObject(ctx, bucket, "someobject", minio.RemoveObjectOptions{}) if err.Error() != "Access Denied." { c.Fatalf("unexpected non-access-denied err: %v", err) } } func (s *TestSuiteIAM) TestSTSWithGroupPolicy(c *check) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket creat error: %v", err) } // Create policy, user and associate policy policy := "mypolicy" policyBytes := []byte(fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::%s/*" ] } ] }`, bucket)) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } accessKey, secretKey := mustGenerateCredentials(c) err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled) if err != nil { c.Fatalf("Unable to set user: %v", err) } // confirm that the user is unable to access the bucket - we have not // yet set any policy uClient := s.getUserClient(c, accessKey, secretKey, "") c.mustNotListObjects(ctx, uClient, bucket) err = s.adm.UpdateGroupMembers(ctx, madmin.GroupAddRemove{ Group: "test-group", Members: []string{accessKey}, }) if err != nil { c.Fatalf("unable to add user to group: %v", err) } err = s.adm.SetPolicy(ctx, policy, "test-group", true) if err != nil { c.Fatalf("Unable to set policy: %v", err) } // confirm that the user is able to access the bucket - permission comes // from group. c.mustListObjects(ctx, uClient, bucket) // Create STS user. assumeRole := cr.STSAssumeRole{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, Options: cr.STSAssumeRoleOptions{ AccessKey: accessKey, SecretKey: secretKey, Location: "", }, } value, err := assumeRole.Retrieve() if err != nil { c.Fatalf("err calling assumeRole: %v", err) } // Check that STS user client has access coming from parent user's // group. minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Validate that the client cannot remove any objects err = minioClient.RemoveObject(ctx, bucket, "someobject", minio.RemoveObjectOptions{}) if err.Error() != "Access Denied." { c.Fatalf("unexpected non-access-denied err: %v", err) } } // TestSTSForRoot - needs to be the first test after server setup due to the // buckets list check. func (s *TestSuiteIAM) TestSTSForRoot(c *check) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } assumeRole := cr.STSAssumeRole{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, Options: cr.STSAssumeRoleOptions{ AccessKey: globalActiveCred.AccessKey, SecretKey: globalActiveCred.SecretKey, Location: "", }, } value, err := assumeRole.Retrieve() if err != nil { c.Fatalf("err calling assumeRole: %v", err) } minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Validate that a bucket can be created bucket2 := getRandomBucketName() err = minioClient.MakeBucket(ctx, bucket2, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket creat error: %v", err) } // Validate that admin APIs can be called - create an madmin client with // user creds userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, }) if err != nil { c.Fatalf("Err creating user admin client: %v", err) } userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport) time.Sleep(2 * time.Second) // wait for listbuckets cache to be invalidated accInfo, err := userAdmClient.AccountInfo(ctx, madmin.AccountOpts{}) if err != nil { c.Fatalf("root user STS should be able to get account info: %v", err) } gotBuckets := set.NewStringSet() for _, b := range accInfo.Buckets { gotBuckets.Add(b.Name) if !(b.Access.Read && b.Access.Write) { c.Fatalf("root user should have read and write access to bucket: %v", b.Name) } } shouldHaveBuckets := set.CreateStringSet(bucket2, bucket) if !gotBuckets.Equals(shouldHaveBuckets) { c.Fatalf("root user should have access to all buckets") } // This must fail. if err := userAdmClient.AddUser(ctx, globalActiveCred.AccessKey, globalActiveCred.SecretKey); err == nil { c.Fatal("AddUser() for root credential must fail via root STS creds") } } // SetUpLDAP - expects to setup an LDAP test server using the test LDAP // container and canned data from https://github.com/minio/minio-ldap-testing func (s *TestSuiteIAM) SetUpLDAP(c *check, serverAddr string) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() configCmds := []string{ "identity_ldap", fmt.Sprintf("server_addr=%s", serverAddr), "server_insecure=on", "lookup_bind_dn=cn=admin,dc=min,dc=io", "lookup_bind_password=admin", "user_dn_search_base_dn=dc=min,dc=io", "user_dn_search_filter=(uid=%s)", "user_dn_attributes=sshPublicKey", "group_search_base_dn=ou=swengg,dc=min,dc=io", "group_search_filter=(&(objectclass=groupofnames)(member=%d))", } _, err := s.adm.SetConfigKV(ctx, strings.Join(configCmds, " ")) if err != nil { c.Fatalf("unable to setup LDAP for tests: %v", err) } s.RestartIAMSuite(c) } // SetUpLDAPWithNonNormalizedBaseDN - expects to setup an LDAP test server using // the test LDAP container and canned data from // https://github.com/minio/minio-ldap-testing // // Sets up non-normalized base DN configuration for testing. func (s *TestSuiteIAM) SetUpLDAPWithNonNormalizedBaseDN(c *check, serverAddr string) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() configCmds := []string{ "identity_ldap", fmt.Sprintf("server_addr=%s", serverAddr), "server_insecure=on", "lookup_bind_dn=cn=admin,dc=min,dc=io", "lookup_bind_password=admin", // `DC` is intentionally capitalized here. "user_dn_search_base_dn=DC=min,DC=io", "user_dn_search_filter=(uid=%s)", // `DC` is intentionally capitalized here. "group_search_base_dn=ou=swengg,DC=min,dc=io", "group_search_filter=(&(objectclass=groupofnames)(member=%d))", } _, err := s.adm.SetConfigKV(ctx, strings.Join(configCmds, " ")) if err != nil { c.Fatalf("unable to setup LDAP for tests: %v", err) } s.RestartIAMSuite(c) } const ( EnvTestLDAPServer = "_MINIO_LDAP_TEST_SERVER" ) func TestIAMWithLDAPServerSuite(t *testing.T) { for i, testCase := range iamTestSuites { t.Run( fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription), func(t *testing.T) { c := &check{t, testCase.serverType} suite := testCase ldapServer := os.Getenv(EnvTestLDAPServer) if ldapServer == "" { c.Skipf("Skipping LDAP test as no LDAP server is provided via %s", EnvTestLDAPServer) } suite.SetUpSuite(c) suite.SetUpLDAP(c, ldapServer) suite.TestLDAPSTS(c) suite.TestLDAPUnicodeVariations(c) suite.TestLDAPSTSServiceAccounts(c) suite.TestLDAPSTSServiceAccountsWithUsername(c) suite.TestLDAPSTSServiceAccountsWithGroups(c) suite.TestLDAPAttributesLookup(c) suite.TestLDAPCyrillicUser(c) suite.TearDownSuite(c) }, ) } } // This test is for a fix added to handle non-normalized base DN values in the // LDAP configuration. It runs the existing LDAP sub-tests with a non-normalized // LDAP configuration. func TestIAMWithLDAPNonNormalizedBaseDNConfigServerSuite(t *testing.T) { for i, testCase := range iamTestSuites { t.Run( fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription), func(t *testing.T) { c := &check{t, testCase.serverType} suite := testCase ldapServer := os.Getenv(EnvTestLDAPServer) if ldapServer == "" { c.Skipf("Skipping LDAP test as no LDAP server is provided via %s", EnvTestLDAPServer) } suite.SetUpSuite(c) suite.SetUpLDAPWithNonNormalizedBaseDN(c, ldapServer) suite.TestLDAPSTS(c) suite.TestLDAPUnicodeVariations(c) suite.TestLDAPSTSServiceAccounts(c) suite.TestLDAPSTSServiceAccountsWithUsername(c) suite.TestLDAPSTSServiceAccountsWithGroups(c) suite.TearDownSuite(c) }, ) } } func TestIAMExportImportWithLDAP(t *testing.T) { for i, testCase := range iamTestSuites { t.Run( fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription), func(t *testing.T) { c := &check{t, testCase.serverType} suite := testCase ldapServer := os.Getenv(EnvTestLDAPServer) if ldapServer == "" { c.Skipf("Skipping LDAP test as no LDAP server is provided via %s", EnvTestLDAPServer) } iamTestContentCases := []iamTestContent{ { policies: map[string][]byte{ "mypolicy": []byte(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject","s3:ListBucket","s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/*"]}]}`), }, ldapUserPolicyMappings: map[string][]string{ "uid=dillon,ou=people,ou=swengg,dc=min,dc=io": {"mypolicy"}, "uid=liza,ou=people,ou=swengg,dc=min,dc=io": {"consoleAdmin"}, }, ldapGroupPolicyMappings: map[string][]string{ "cn=projectb,ou=groups,ou=swengg,dc=min,dc=io": {"mypolicy"}, "cn=projecta,ou=groups,ou=swengg,dc=min,dc=io": {"consoleAdmin"}, }, }, } for caseNum, content := range iamTestContentCases { suite.SetUpSuite(c) suite.SetUpLDAP(c, ldapServer) exportedContent := suite.TestIAMExport(c, caseNum, content) suite.TearDownSuite(c) suite.SetUpSuite(c) suite.SetUpLDAP(c, ldapServer) suite.TestIAMImport(c, exportedContent, caseNum, content) suite.TearDownSuite(c) } }, ) } } func TestIAMImportAssetWithLDAP(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() exportContentStrings := map[string]string{ allPoliciesFile: `{"consoleAdmin":{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["admin:*"]},{"Effect":"Allow","Action":["kms:*"]},{"Effect":"Allow","Action":["s3:*"],"Resource":["arn:aws:s3:::*"]}]},"diagnostics":{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["admin:Prometheus","admin:Profiling","admin:ServerTrace","admin:ConsoleLog","admin:ServerInfo","admin:TopLocksInfo","admin:OBDInfo","admin:BandwidthMonitor"],"Resource":["arn:aws:s3:::*"]}]},"readonly":{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetBucketLocation","s3:GetObject"],"Resource":["arn:aws:s3:::*"]}]},"readwrite":{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:*"],"Resource":["arn:aws:s3:::*"]}]},"writeonly":{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:PutObject"],"Resource":["arn:aws:s3:::*"]}]}}`, // Built-in user should be imported without errors even if LDAP is // enabled. allUsersFile: `{ "foo": { "secretKey": "foobar123", "status": "enabled" } } `, // Built-in groups should be imported without errors even if LDAP is // enabled. allGroupsFile: `{ "mygroup": { "version": 1, "status": "enabled", "members": [ "foo" ], "updatedAt": "2024-04-23T21:34:43.587429659Z" } } `, // The `cn=projecty,..` group below is not under a configured DN, but we // should still import without an error. allSvcAcctsFile: `{ "u4ccRswj62HV3Ifwima7": { "parent": "uid=svc.algorithm,OU=swengg,DC=min,DC=io", "accessKey": "u4ccRswj62HV3Ifwima7", "secretKey": "ZoEoZdLlzVbOlT9rbhD7ZN7TLyiYXSAlB79uGEge", "groups": ["cn=project.c,ou=groups,OU=swengg,DC=min,DC=io", "cn=projecty,ou=groups,ou=hwengg,dc=min,dc=io"], "claims": { "accessKey": "u4ccRswj62HV3Ifwima7", "ldapUser": "uid=svc.algorithm,ou=swengg,dc=min,dc=io", "ldapUsername": "svc.algorithm", "parent": "uid=svc.algorithm,ou=swengg,dc=min,dc=io", "sa-policy": "inherited-policy" }, "sessionPolicy": null, "status": "on", "name": "", "description": "" } } `, // Built-in user-to-policies mapping should be imported without errors // even if LDAP is enabled. userPolicyMappingsFile: `{ "foo": { "version": 0, "policy": "readwrite", "updatedAt": "2024-04-23T21:34:43.815519816Z" } } `, // Contains: // // 1. duplicate mapping with same policy, we should not error out; // // 2. non-LDAP group mapping, we should not error out; groupPolicyMappingsFile: `{ "cn=project.c,ou=groups,ou=swengg,DC=min,dc=io": { "version": 0, "policy": "consoleAdmin", "updatedAt": "2024-04-17T23:54:28.442998301Z" }, "mygroup": { "version": 0, "policy": "consoleAdmin", "updatedAt": "2024-04-23T21:34:43.66922872Z" }, "cn=project.c,ou=groups,OU=swengg,DC=min,DC=io": { "version": 0, "policy": "consoleAdmin", "updatedAt": "2024-04-17T20:54:28.442998301Z" } } `, stsUserPolicyMappingsFile: `{ "uid=dillon,ou=people,OU=swengg,DC=min,DC=io": { "version": 0, "policy": "consoleAdmin", "updatedAt": "2024-04-17T23:54:10.606645642Z" } } `, } exportContent := map[string][]byte{} for k, v := range exportContentStrings { exportContent[k] = []byte(v) } var importContent []byte { var b bytes.Buffer zipWriter := zip.NewWriter(&b) rawDataFn := func(r io.Reader, filename string, sz int) error { header, zerr := zip.FileInfoHeader(dummyFileInfo{ name: filename, size: int64(sz), mode: 0o600, modTime: time.Now(), isDir: false, sys: nil, }) if zerr != nil { adminLogIf(ctx, zerr) return nil } header.Method = zip.Deflate zwriter, zerr := zipWriter.CreateHeader(header) if zerr != nil { adminLogIf(ctx, zerr) return nil } if _, err := io.Copy(zwriter, r); err != nil { adminLogIf(ctx, err) } return nil } for _, f := range iamExportFiles { iamFile := pathJoin(iamAssetsDir, f) fileContent, ok := exportContent[f] if !ok { t.Fatalf("missing content for %s", f) } if err := rawDataFn(bytes.NewReader(fileContent), iamFile, len(fileContent)); err != nil { t.Fatalf("failed to write %s: %v", iamFile, err) } } zipWriter.Close() importContent = b.Bytes() } for i, testCase := range iamTestSuites { t.Run( fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription), func(t *testing.T) { c := &check{t, testCase.serverType} suite := testCase ldapServer := os.Getenv(EnvTestLDAPServer) if ldapServer == "" { c.Skipf("Skipping LDAP test as no LDAP server is provided via %s", EnvTestLDAPServer) } suite.SetUpSuite(c) suite.SetUpLDAP(c, ldapServer) suite.TestIAMImportAssetContent(c, importContent) suite.TearDownSuite(c) }, ) } } type iamTestContent struct { policies map[string][]byte ldapUserPolicyMappings map[string][]string ldapGroupPolicyMappings map[string][]string } func (s *TestSuiteIAM) TestIAMExport(c *check, caseNum int, content iamTestContent) []byte { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() for policy, policyBytes := range content.policies { err := s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("export %d: policy add error: %v", caseNum, err) } } for userDN, policies := range content.ldapUserPolicyMappings { _, err := s.adm.AttachPolicyLDAP(ctx, madmin.PolicyAssociationReq{ Policies: policies, User: userDN, }) if err != nil { c.Fatalf("export %d: Unable to attach policy: %v", caseNum, err) } } for groupDN, policies := range content.ldapGroupPolicyMappings { _, err := s.adm.AttachPolicyLDAP(ctx, madmin.PolicyAssociationReq{ Policies: policies, Group: groupDN, }) if err != nil { c.Fatalf("export %d: Unable to attach group policy: %v", caseNum, err) } } contentReader, err := s.adm.ExportIAM(ctx) if err != nil { c.Fatalf("export %d: Unable to export IAM: %v", caseNum, err) } defer contentReader.Close() expContent, err := io.ReadAll(contentReader) if err != nil { c.Fatalf("export %d: Unable to read exported content: %v", caseNum, err) } return expContent } type dummyCloser struct { io.Reader } func (d dummyCloser) Close() error { return nil } func (s *TestSuiteIAM) TestIAMImportAssetContent(c *check, content []byte) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() dummyCloser := dummyCloser{bytes.NewReader(content)} err := s.adm.ImportIAM(ctx, dummyCloser) if err != nil { c.Fatalf("Unable to import IAM: %v", err) } entRes, err := s.adm.GetLDAPPolicyEntities(ctx, madmin.PolicyEntitiesQuery{}) if err != nil { c.Fatalf("Unable to get policy entities: %v", err) } expected := madmin.PolicyEntitiesResult{ PolicyMappings: []madmin.PolicyEntities{ { Policy: "consoleAdmin", Users: []string{"uid=dillon,ou=people,ou=swengg,dc=min,dc=io"}, Groups: []string{"cn=project.c,ou=groups,ou=swengg,dc=min,dc=io"}, }, }, } entRes.Timestamp = time.Time{} if !reflect.DeepEqual(expected, entRes) { c.Fatalf("policy entities mismatch: expected: %v, got: %v", expected, entRes) } dn := "uid=svc.algorithm,ou=swengg,dc=min,dc=io" res, err := s.adm.ListAccessKeysLDAP(ctx, dn, "") if err != nil { c.Fatalf("Unable to list access keys: %v", err) } epochTime := time.Unix(0, 0).UTC() expectedAccKeys := madmin.ListAccessKeysLDAPResp{ ServiceAccounts: []madmin.ServiceAccountInfo{ { AccessKey: "u4ccRswj62HV3Ifwima7", Expiration: &epochTime, }, }, } if !reflect.DeepEqual(expectedAccKeys, res) { c.Fatalf("access keys mismatch: expected: %v, got: %v", expectedAccKeys, res) } accKeyInfo, err := s.adm.InfoServiceAccount(ctx, "u4ccRswj62HV3Ifwima7") if err != nil { c.Fatalf("Unable to get service account info: %v", err) } if accKeyInfo.ParentUser != "uid=svc.algorithm,ou=swengg,dc=min,dc=io" { c.Fatalf("parent mismatch: expected: %s, got: %s", "uid=svc.algorithm,ou=swengg,dc=min,dc=io", accKeyInfo.ParentUser) } } func (s *TestSuiteIAM) TestIAMImport(c *check, exportedContent []byte, caseNum int, content iamTestContent) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() dummyCloser := dummyCloser{bytes.NewReader(exportedContent)} err := s.adm.ImportIAM(ctx, dummyCloser) if err != nil { c.Fatalf("import %d: Unable to import IAM: %v", caseNum, err) } gotContent := iamTestContent{ policies: make(map[string][]byte), ldapUserPolicyMappings: make(map[string][]string), ldapGroupPolicyMappings: make(map[string][]string), } policyContentMap, err := s.adm.ListCannedPolicies(ctx) if err != nil { c.Fatalf("import %d: Unable to list policies: %v", caseNum, err) } defaultCannedPolicies := set.CreateStringSet("consoleAdmin", "readwrite", "readonly", "diagnostics", "writeonly") for policy, policyBytes := range policyContentMap { if defaultCannedPolicies.Contains(policy) { continue } gotContent.policies[policy] = policyBytes } policyQueryRes, err := s.adm.GetLDAPPolicyEntities(ctx, madmin.PolicyEntitiesQuery{}) if err != nil { c.Fatalf("import %d: Unable to get policy entities: %v", caseNum, err) } for _, entity := range policyQueryRes.PolicyMappings { m := gotContent.ldapUserPolicyMappings for _, user := range entity.Users { m[user] = append(m[user], entity.Policy) } m = gotContent.ldapGroupPolicyMappings for _, group := range entity.Groups { m[group] = append(m[group], entity.Policy) } } { // We don't compare the values of the canned policies because server is // re-encoding them. (FIXME?) for k := range content.policies { content.policies[k] = nil gotContent.policies[k] = nil } if !reflect.DeepEqual(content.policies, gotContent.policies) { c.Fatalf("import %d: policies mismatch: expected: %v, got: %v", caseNum, content.policies, gotContent.policies) } } if !reflect.DeepEqual(content.ldapUserPolicyMappings, gotContent.ldapUserPolicyMappings) { c.Fatalf("import %d: user policy mappings mismatch: expected: %v, got: %v", caseNum, content.ldapUserPolicyMappings, gotContent.ldapUserPolicyMappings) } if !reflect.DeepEqual(content.ldapGroupPolicyMappings, gotContent.ldapGroupPolicyMappings) { c.Fatalf("import %d: group policy mappings mismatch: expected: %v, got: %v", caseNum, content.ldapGroupPolicyMappings, gotContent.ldapGroupPolicyMappings) } } func (s *TestSuiteIAM) TestLDAPSTS(c *check) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Create policy policy := "mypolicy" policyBytes := []byte(fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::%s/*" ] } ] }`, bucket)) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } ldapID := cr.LDAPIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, LDAPUsername: "dillon", LDAPPassword: "dillon", } _, err = ldapID.Retrieve() if err == nil { c.Fatalf("Expected to fail to create STS cred with no associated policy!") } // Attempting to set a non-existent policy should fail. userDN := "uid=dillon,ou=people,ou=swengg,dc=min,dc=io" err = s.adm.SetPolicy(ctx, policy+"x", userDN, false) if err == nil { c.Fatalf("should not be able to set non-existent policy") } err = s.adm.SetPolicy(ctx, policy, userDN, false) if err != nil { c.Fatalf("Unable to set policy: %v", err) } value, err := ldapID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that user listing does not return any entries usersList, err := s.adm.ListUsers(ctx) if err != nil { c.Fatalf("list users should not fail: %v", err) } if len(usersList) != 1 { c.Fatalf("expected user listing output: %v", usersList) } uinfo := usersList[userDN] if uinfo.PolicyName != policy || uinfo.Status != madmin.AccountEnabled { c.Fatalf("expected user listing content: %v", uinfo) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Validate that the client cannot remove any objects err = minioClient.RemoveObject(ctx, bucket, "someobject", minio.RemoveObjectOptions{}) if err.Error() != "Access Denied." { c.Fatalf("unexpected non-access-denied err: %v", err) } // Remove the policy assignment on the user DN: err = s.adm.SetPolicy(ctx, "", userDN, false) if err != nil { c.Fatalf("Unable to remove policy setting: %v", err) } _, err = ldapID.Retrieve() if err == nil { c.Fatalf("Expected to fail to create a user with no associated policy!") } // Set policy via group and validate policy assignment. groupDN := "cn=projectb,ou=groups,ou=swengg,dc=min,dc=io" err = s.adm.SetPolicy(ctx, policy, groupDN, true) if err != nil { c.Fatalf("Unable to set group policy: %v", err) } value, err = ldapID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } minioClient, err = minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Validate that the client cannot remove any objects err = minioClient.RemoveObject(ctx, bucket, "someobject", minio.RemoveObjectOptions{}) c.Assert(err.Error(), "Access Denied.") } func (s *TestSuiteIAM) TestLDAPUnicodeVariationsLegacyAPI(c *check) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Create policy policy := "mypolicy" policyBytes := []byte(fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::%s/*" ] } ] }`, bucket)) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } ldapID := cr.LDAPIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, LDAPUsername: "svc.algorithm", LDAPPassword: "example", } _, err = ldapID.Retrieve() if err == nil { c.Fatalf("Expected to fail to create STS cred with no associated policy!") } mustNormalizeDN := func(dn string) string { normalizedDN, err := ldap.NormalizeDN(dn) if err != nil { c.Fatalf("normalize err: %v", err) } return normalizedDN } actualUserDN := mustNormalizeDN("uid=svc.algorithm,OU=swengg,DC=min,DC=io") // \uFE52 is the unicode dot SMALL FULL STOP used below: userDNWithUnicodeDot := "uid=svc﹒algorithm,OU=swengg,DC=min,DC=io" if err = s.adm.SetPolicy(ctx, policy, userDNWithUnicodeDot, false); err != nil { c.Fatalf("Unable to set policy: %v", err) } value, err := ldapID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } usersList, err := s.adm.ListUsers(ctx) if err != nil { c.Fatalf("list users should not fail: %v", err) } if len(usersList) != 1 { c.Fatalf("expected user listing output: %#v", usersList) } uinfo := usersList[actualUserDN] if uinfo.PolicyName != policy || uinfo.Status != madmin.AccountEnabled { c.Fatalf("expected user listing content: %v", uinfo) } minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Validate that the client cannot remove any objects err = minioClient.RemoveObject(ctx, bucket, "someobject", minio.RemoveObjectOptions{}) if err.Error() != "Access Denied." { c.Fatalf("unexpected non-access-denied err: %v", err) } // Remove the policy assignment on the user DN: if err = s.adm.SetPolicy(ctx, "", userDNWithUnicodeDot, false); err != nil { c.Fatalf("Unable to remove policy setting: %v", err) } _, err = ldapID.Retrieve() if err == nil { c.Fatalf("Expected to fail to create a user with no associated policy!") } // Set policy via group and validate policy assignment. actualGroupDN := mustNormalizeDN("cn=project.c,ou=groups,ou=swengg,dc=min,dc=io") groupDNWithUnicodeDot := "cn=project﹒c,ou=groups,ou=swengg,dc=min,dc=io" if err = s.adm.SetPolicy(ctx, policy, groupDNWithUnicodeDot, true); err != nil { c.Fatalf("Unable to attach group policy: %v", err) } value, err = ldapID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } policyResult, err := s.adm.GetLDAPPolicyEntities(ctx, madmin.PolicyEntitiesQuery{ Policy: []string{policy}, }) if err != nil { c.Fatalf("GetLDAPPolicyEntities should not fail: %v", err) } { // Check that the mapping we created exists. idx := slices.IndexFunc(policyResult.PolicyMappings, func(e madmin.PolicyEntities) bool { return e.Policy == policy && slices.Contains(e.Groups, actualGroupDN) }) if !(idx >= 0) { c.Fatalf("expected groupDN (%s) to be present in mapping list: %#v", actualGroupDN, policyResult) } } minioClient, err = minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Validate that the client cannot remove any objects err = minioClient.RemoveObject(ctx, bucket, "someobject", minio.RemoveObjectOptions{}) c.Assert(err.Error(), "Access Denied.") } func (s *TestSuiteIAM) TestLDAPUnicodeVariations(c *check) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Create policy policy := "mypolicy" policyBytes := []byte(fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::%s/*" ] } ] }`, bucket)) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } ldapID := cr.LDAPIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, LDAPUsername: "svc.algorithm", LDAPPassword: "example", } _, err = ldapID.Retrieve() if err == nil { c.Fatalf("Expected to fail to create STS cred with no associated policy!") } mustNormalizeDN := func(dn string) string { normalizedDN, err := ldap.NormalizeDN(dn) if err != nil { c.Fatalf("normalize err: %v", err) } return normalizedDN } actualUserDN := mustNormalizeDN("uid=svc.algorithm,OU=swengg,DC=min,DC=io") // \uFE52 is the unicode dot SMALL FULL STOP used below: userDNWithUnicodeDot := "uid=svc﹒algorithm,OU=swengg,DC=min,DC=io" _, err = s.adm.AttachPolicyLDAP(ctx, madmin.PolicyAssociationReq{ Policies: []string{policy}, User: userDNWithUnicodeDot, }) if err != nil { c.Fatalf("Unable to set policy: %v", err) } value, err := ldapID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } usersList, err := s.adm.ListUsers(ctx) if err != nil { c.Fatalf("list users should not fail: %v", err) } if len(usersList) != 1 { c.Fatalf("expected user listing output: %#v", usersList) } uinfo := usersList[actualUserDN] if uinfo.PolicyName != policy || uinfo.Status != madmin.AccountEnabled { c.Fatalf("expected user listing content: %v", uinfo) } minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Validate that the client cannot remove any objects err = minioClient.RemoveObject(ctx, bucket, "someobject", minio.RemoveObjectOptions{}) if err.Error() != "Access Denied." { c.Fatalf("unexpected non-access-denied err: %v", err) } // Remove the policy assignment on the user DN: _, err = s.adm.DetachPolicyLDAP(ctx, madmin.PolicyAssociationReq{ Policies: []string{policy}, User: userDNWithUnicodeDot, }) if err != nil { c.Fatalf("Unable to remove policy setting: %v", err) } _, err = ldapID.Retrieve() if err == nil { c.Fatalf("Expected to fail to create a user with no associated policy!") } // Set policy via group and validate policy assignment. actualGroupDN := mustNormalizeDN("cn=project.c,ou=groups,ou=swengg,dc=min,dc=io") groupDNWithUnicodeDot := "cn=project﹒c,ou=groups,ou=swengg,dc=min,dc=io" _, err = s.adm.AttachPolicyLDAP(ctx, madmin.PolicyAssociationReq{ Policies: []string{policy}, Group: groupDNWithUnicodeDot, }) if err != nil { c.Fatalf("Unable to attach group policy: %v", err) } value, err = ldapID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } policyResult, err := s.adm.GetLDAPPolicyEntities(ctx, madmin.PolicyEntitiesQuery{ Policy: []string{policy}, }) if err != nil { c.Fatalf("GetLDAPPolicyEntities should not fail: %v", err) } { // Check that the mapping we created exists. idx := slices.IndexFunc(policyResult.PolicyMappings, func(e madmin.PolicyEntities) bool { return e.Policy == policy && slices.Contains(e.Groups, actualGroupDN) }) if !(idx >= 0) { c.Fatalf("expected groupDN (%s) to be present in mapping list: %#v", actualGroupDN, policyResult) } } minioClient, err = minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Validate that the client cannot remove any objects err = minioClient.RemoveObject(ctx, bucket, "someobject", minio.RemoveObjectOptions{}) c.Assert(err.Error(), "Access Denied.") } func (s *TestSuiteIAM) TestLDAPSTSServiceAccounts(c *check) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Create policy policy := "mypolicy" policyBytes := []byte(fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::%s/*" ] } ] }`, bucket)) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } userDN := "uid=dillon,ou=people,ou=swengg,dc=min,dc=io" err = s.adm.SetPolicy(ctx, policy, userDN, false) if err != nil { c.Fatalf("Unable to set policy: %v", err) } ldapID := cr.LDAPIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, LDAPUsername: "dillon", LDAPPassword: "dillon", } value, err := ldapID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } // Check that the LDAP sts cred is actually working. minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Create an madmin client with user creds userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, }) if err != nil { c.Fatalf("Err creating user admin client: %v", err) } userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport) // Create svc acc cr := c.mustCreateSvcAccount(ctx, value.AccessKeyID, userAdmClient) // 1. Check that svc account appears in listing c.assertSvcAccAppearsInListing(ctx, userAdmClient, value.AccessKeyID, cr.AccessKey) // 2. Check that svc account info can be queried c.assertSvcAccInfoQueryable(ctx, userAdmClient, value.AccessKeyID, cr.AccessKey, true) // 3. Check S3 access c.assertSvcAccS3Access(ctx, s, cr, bucket) // 5. Check that service account can be deleted. c.assertSvcAccDeletion(ctx, s, userAdmClient, value.AccessKeyID, bucket) // 6. Check that service account cannot be created for some other user. c.mustNotCreateSvcAccount(ctx, globalActiveCred.AccessKey, userAdmClient) } func (s *TestSuiteIAM) TestLDAPSTSServiceAccountsWithUsername(c *check) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() bucket := "dillon" err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Create policy policy := "mypolicy-username" policyBytes := []byte(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::${ldap:username}/*" ] } ] }`) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } userDN := "uid=dillon,ou=people,ou=swengg,dc=min,dc=io" err = s.adm.SetPolicy(ctx, policy, userDN, false) if err != nil { c.Fatalf("Unable to set policy: %v", err) } ldapID := cr.LDAPIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, LDAPUsername: "dillon", LDAPPassword: "dillon", } value, err := ldapID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } // Check that the LDAP sts cred is actually working. minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Create an madmin client with user creds userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, }) if err != nil { c.Fatalf("Err creating user admin client: %v", err) } userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport) // Create svc acc cr := c.mustCreateSvcAccount(ctx, value.AccessKeyID, userAdmClient) svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "") // 1. Check S3 access for service account ListObjects() c.mustListObjects(ctx, svcClient, bucket) // 2. Check S3 access for upload c.mustUpload(ctx, svcClient, bucket) // 3. Check S3 access for download c.mustDownload(ctx, svcClient, bucket) } // In this test, the parent users gets their permissions from a group, rather // than having a policy set directly on them. func (s *TestSuiteIAM) TestLDAPSTSServiceAccountsWithGroups(c *check) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Create policy policy := "mypolicy" policyBytes := []byte(fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::%s/*" ] } ] }`, bucket)) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } groupDN := "cn=projecta,ou=groups,ou=swengg,dc=min,dc=io" err = s.adm.SetPolicy(ctx, policy, groupDN, true) if err != nil { c.Fatalf("Unable to set policy: %v", err) } ldapID := cr.LDAPIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, LDAPUsername: "dillon", LDAPPassword: "dillon", } value, err := ldapID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } // Check that the LDAP sts cred is actually working. minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Create an madmin client with user creds userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, }) if err != nil { c.Fatalf("Err creating user admin client: %v", err) } userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport) // Create svc acc cr := c.mustCreateSvcAccount(ctx, value.AccessKeyID, userAdmClient) // 1. Check that svc account appears in listing c.assertSvcAccAppearsInListing(ctx, userAdmClient, value.AccessKeyID, cr.AccessKey) // 2. Check that svc account info can be queried c.assertSvcAccInfoQueryable(ctx, userAdmClient, value.AccessKeyID, cr.AccessKey, true) // 3. Check S3 access c.assertSvcAccS3Access(ctx, s, cr, bucket) // 5. Check that service account can be deleted. c.assertSvcAccDeletion(ctx, s, userAdmClient, value.AccessKeyID, bucket) // 6. Check that service account cannot be created for some other user. c.mustNotCreateSvcAccount(ctx, globalActiveCred.AccessKey, userAdmClient) } func (s *TestSuiteIAM) TestLDAPCyrillicUser(c *check) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _, err := s.adm.AttachPolicyLDAP(ctx, madmin.PolicyAssociationReq{ Policies: []string{"readwrite"}, User: "uid=Пользователь,ou=people,ou=swengg,dc=min,dc=io", }) if err != nil { c.Fatalf("Unable to set policy: %v", err) } cases := []struct { username string dn string }{ { username: "Пользователь", dn: "uid=Пользователь,ou=people,ou=swengg,dc=min,dc=io", }, } conn, err := globalIAMSys.LDAPConfig.LDAP.Connect() if err != nil { c.Fatalf("LDAP connect failed: %v", err) } defer conn.Close() for i, testCase := range cases { ldapID := cr.LDAPIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, LDAPUsername: testCase.username, LDAPPassword: "example", } value, err := ldapID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } // Retrieve the STS account's credential object. u, ok := globalIAMSys.GetUser(ctx, value.AccessKeyID) if !ok { c.Fatalf("Expected to find user %s", value.AccessKeyID) } if u.Credentials.AccessKey != value.AccessKeyID { c.Fatalf("Expected access key %s, got %s", value.AccessKeyID, u.Credentials.AccessKey) } // Retrieve the credential's claims. secret, err := getTokenSigningKey() if err != nil { c.Fatalf("Error getting token signing key: %v", err) } claims, err := getClaimsFromTokenWithSecret(value.SessionToken, secret) if err != nil { c.Fatalf("Error getting claims from token: %v", err) } // Validate claims. dnClaim := claims[ldapActualUser].(string) if dnClaim != testCase.dn { c.Fatalf("Test %d: unexpected dn claim: %s", i+1, dnClaim) } } } func (s *TestSuiteIAM) TestLDAPAttributesLookup(c *check) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() groupDN := "cn=projectb,ou=groups,ou=swengg,dc=min,dc=io" _, err := s.adm.AttachPolicyLDAP(ctx, madmin.PolicyAssociationReq{ Policies: []string{"readwrite"}, Group: groupDN, }) if err != nil { c.Fatalf("Unable to set policy: %v", err) } cases := []struct { username string dn string expectedSSHKeyType string }{ { username: "dillon", dn: "uid=dillon,ou=people,ou=swengg,dc=min,dc=io", expectedSSHKeyType: "ssh-ed25519", }, { username: "liza", dn: "uid=liza,ou=people,ou=swengg,dc=min,dc=io", expectedSSHKeyType: "ssh-rsa", }, } conn, err := globalIAMSys.LDAPConfig.LDAP.Connect() if err != nil { c.Fatalf("LDAP connect failed: %v", err) } defer conn.Close() for i, testCase := range cases { ldapID := cr.LDAPIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, LDAPUsername: testCase.username, LDAPPassword: testCase.username, } value, err := ldapID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } // Retrieve the STS account's credential object. u, ok := globalIAMSys.GetUser(ctx, value.AccessKeyID) if !ok { c.Fatalf("Expected to find user %s", value.AccessKeyID) } if u.Credentials.AccessKey != value.AccessKeyID { c.Fatalf("Expected access key %s, got %s", value.AccessKeyID, u.Credentials.AccessKey) } // Retrieve the credential's claims. secret, err := getTokenSigningKey() if err != nil { c.Fatalf("Error getting token signing key: %v", err) } claims, err := getClaimsFromTokenWithSecret(value.SessionToken, secret) if err != nil { c.Fatalf("Error getting claims from token: %v", err) } // Validate claims. Check if the sshPublicKey claim is present. dnClaim := claims[ldapActualUser].(string) if dnClaim != testCase.dn { c.Fatalf("Test %d: unexpected dn claim: %s", i+1, dnClaim) } sshPublicKeyClaim := claims[ldapAttribPrefix+"sshPublicKey"].([]interface{})[0].(string) if sshPublicKeyClaim == "" { c.Fatalf("Test %d: expected sshPublicKey claim to be present", i+1) } parts := strings.Split(sshPublicKeyClaim, " ") if parts[0] != testCase.expectedSSHKeyType { c.Fatalf("Test %d: unexpected sshPublicKey type: %s", i+1, parts[0]) } } } func (s *TestSuiteIAM) TestOpenIDSTS(c *check) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Generate web identity STS token by interacting with OpenID IDP. token, err := MockOpenIDTestUserInteraction(ctx, testAppParams, "dillon@example.io", "dillon") if err != nil { c.Fatalf("mock user err: %v", err) } // fmt.Printf("TOKEN: %s\n", token) webID := cr.STSWebIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, GetWebIDTokenExpiry: func() (*cr.WebIdentityToken, error) { return &cr.WebIdentityToken{ Token: token, }, nil }, } // Create policy - with name as one of the groups in OpenID the user is // a member of. policy := "projecta" policyBytes := []byte(fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::%s/*" ] } ] }`, bucket)) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } value, err := webID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Validate that the client cannot remove any objects err = minioClient.RemoveObject(ctx, bucket, "someobject", minio.RemoveObjectOptions{}) if err.Error() != "Access Denied." { c.Fatalf("unexpected non-access-denied err: %v", err) } } func (s *TestSuiteIAM) TestOpenIDSTSDurationSeconds(c *check) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Generate web identity STS token by interacting with OpenID IDP. token, err := MockOpenIDTestUserInteraction(ctx, testAppParams, "dillon@example.io", "dillon") if err != nil { c.Fatalf("mock user err: %v", err) } // fmt.Printf("TOKEN: %s\n", token) webID := cr.STSWebIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, GetWebIDTokenExpiry: func() (*cr.WebIdentityToken, error) { return &cr.WebIdentityToken{ Token: token, Expiry: 900, }, nil }, } // Create policy - with name as one of the groups in OpenID the user is // a member of. policy := "projecta" policyTmpl := `{ "Version": "2012-10-17", "Statement": [ { "Effect": "Deny", "Action": ["sts:AssumeRoleWithWebIdentity"], "Condition": {"NumericGreaterThan": {"sts:DurationSeconds": "%d"}} }, { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::%s/*" ] } ] }` for i, testCase := range []struct { durSecs int expectedErr bool }{ {60, true}, {1800, false}, } { policyBytes := []byte(fmt.Sprintf(policyTmpl, testCase.durSecs, bucket)) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("Test %d: policy add error: %v", i+1, err) } value, err := webID.Retrieve() if err != nil && !testCase.expectedErr { c.Fatalf("Test %d: Expected to generate STS creds, got err: %#v", i+1, err) } if err == nil && testCase.expectedErr { c.Fatalf("Test %d: An error is unexpected to generate STS creds, got err: %#v", i+1, err) } if err != nil && testCase.expectedErr { continue } minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Test %d: Error initializing client: %v", i+1, err) } c.mustListObjects(ctx, minioClient, bucket) } } func (s *TestSuiteIAM) TestOpenIDSTSAddUser(c *check) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Generate web identity STS token by interacting with OpenID IDP. token, err := MockOpenIDTestUserInteraction(ctx, testAppParams, "dillon@example.io", "dillon") if err != nil { c.Fatalf("mock user err: %v", err) } webID := cr.STSWebIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, GetWebIDTokenExpiry: func() (*cr.WebIdentityToken, error) { return &cr.WebIdentityToken{ Token: token, }, nil }, } // Create policy - with name as one of the groups in OpenID the user is // a member of. policy := "projecta" policyBytes := []byte(fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::%s/*" ] } ] }`, bucket)) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } value, err := webID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } // Create an madmin client with user creds userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, }) if err != nil { c.Fatalf("Err creating user admin client: %v", err) } userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport) c.mustNotCreateIAMUser(ctx, userAdmClient) // Create admin user policy. policyBytes = []byte(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "admin:*" ] } ] }`) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } cr := c.mustCreateIAMUser(ctx, userAdmClient) userInfo := c.mustGetIAMUserInfo(ctx, userAdmClient, cr.AccessKey) c.Assert(userInfo.Status, madmin.AccountEnabled) } func (s *TestSuiteIAM) TestOpenIDServiceAcc(c *check) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Generate web identity STS token by interacting with OpenID IDP. token, err := MockOpenIDTestUserInteraction(ctx, testAppParams, "dillon@example.io", "dillon") if err != nil { c.Fatalf("mock user err: %v", err) } webID := cr.STSWebIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, GetWebIDTokenExpiry: func() (*cr.WebIdentityToken, error) { return &cr.WebIdentityToken{ Token: token, }, nil }, } // Create policy - with name as one of the groups in OpenID the user is // a member of. policy := "projecta" policyBytes := []byte(fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::%s/*" ] } ] }`, bucket)) err = s.adm.AddCannedPolicy(ctx, policy, policyBytes) if err != nil { c.Fatalf("policy add error: %v", err) } value, err := webID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } // Create an madmin client with user creds userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, }) if err != nil { c.Fatalf("Err creating user admin client: %v", err) } userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport) // Create svc acc cr := c.mustCreateSvcAccount(ctx, value.AccessKeyID, userAdmClient) // 1. Check that svc account appears in listing c.assertSvcAccAppearsInListing(ctx, userAdmClient, value.AccessKeyID, cr.AccessKey) // 2. Check that svc account info can be queried c.assertSvcAccInfoQueryable(ctx, userAdmClient, value.AccessKeyID, cr.AccessKey, true) // 3. Check S3 access c.assertSvcAccS3Access(ctx, s, cr, bucket) // 5. Check that service account can be deleted. c.assertSvcAccDeletion(ctx, s, userAdmClient, value.AccessKeyID, bucket) // 6. Check that service account cannot be created for some other user. c.mustNotCreateSvcAccount(ctx, globalActiveCred.AccessKey, userAdmClient) } var testAppParams = OpenIDClientAppParams{ ClientID: "minio-client-app", ClientSecret: "minio-client-app-secret", ProviderURL: "http://127.0.0.1:5556/dex", RedirectURL: "http://127.0.0.1:10000/oauth_callback", } const ( EnvTestOpenIDServer = "_MINIO_OPENID_TEST_SERVER" EnvTestOpenIDServer2 = "_MINIO_OPENID_TEST_SERVER_2" ) // SetUpOpenIDs - sets up one or more OpenID test servers using the test OpenID // container and canned data from https://github.com/minio/minio-ldap-testing // // Each set of client app params corresponds to a separate openid server, and // the i-th server in this will be applied the i-th policy in `rolePolicies`. If // a rolePolicies entry is an empty string, that server will be configured as // policy-claim based openid server. NOTE that a valid configuration can have a // policy claim based provider only if it is the only OpenID provider. func (s *TestSuiteIAM) SetUpOpenIDs(c *check, testApps []OpenIDClientAppParams, rolePolicies []string) error { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() for i, testApp := range testApps { configCmds := []string{ fmt.Sprintf("identity_openid:%d", i), fmt.Sprintf("config_url=%s/.well-known/openid-configuration", testApp.ProviderURL), fmt.Sprintf("client_id=%s", testApp.ClientID), fmt.Sprintf("client_secret=%s", testApp.ClientSecret), "scopes=openid,groups", fmt.Sprintf("redirect_uri=%s", testApp.RedirectURL), } if rolePolicies[i] != "" { configCmds = append(configCmds, fmt.Sprintf("role_policy=%s", rolePolicies[i])) } else { configCmds = append(configCmds, "claim_name=groups") } _, err := s.adm.SetConfigKV(ctx, strings.Join(configCmds, " ")) if err != nil { return fmt.Errorf("unable to setup OpenID for tests: %v", err) } } s.RestartIAMSuite(c) return nil } // SetUpOpenID - expects to setup an OpenID test server using the test OpenID // container and canned data from https://github.com/minio/minio-ldap-testing func (s *TestSuiteIAM) SetUpOpenID(c *check, serverAddr string, rolePolicy string) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() configCmds := []string{ "identity_openid", fmt.Sprintf("config_url=%s/.well-known/openid-configuration", serverAddr), "client_id=minio-client-app", "client_secret=minio-client-app-secret", "scopes=openid,groups", "redirect_uri=http://127.0.0.1:10000/oauth_callback", } if rolePolicy != "" { configCmds = append(configCmds, fmt.Sprintf("role_policy=%s", rolePolicy)) } else { configCmds = append(configCmds, "claim_name=groups") } _, err := s.adm.SetConfigKV(ctx, strings.Join(configCmds, " ")) if err != nil { c.Fatalf("unable to setup OpenID for tests: %v", err) } s.RestartIAMSuite(c) } func TestIAMWithOpenIDServerSuite(t *testing.T) { for i, testCase := range iamTestSuites { t.Run( fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription), func(t *testing.T) { c := &check{t, testCase.serverType} suite := testCase openIDServer := os.Getenv(EnvTestOpenIDServer) if openIDServer == "" { c.Skip("Skipping OpenID test as no OpenID server is provided.") } suite.SetUpSuite(c) suite.SetUpOpenID(c, openIDServer, "") suite.TestOpenIDSTS(c) suite.TestOpenIDSTSDurationSeconds(c) suite.TestOpenIDServiceAcc(c) suite.TestOpenIDSTSAddUser(c) suite.TearDownSuite(c) }, ) } } func TestIAMWithOpenIDWithRolePolicyServerSuite(t *testing.T) { for i, testCase := range iamTestSuites { t.Run( fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription), func(t *testing.T) { c := &check{t, testCase.serverType} suite := testCase openIDServer := os.Getenv(EnvTestOpenIDServer) if openIDServer == "" { c.Skip("Skipping OpenID test as no OpenID server is provided.") } suite.SetUpSuite(c) suite.SetUpOpenID(c, openIDServer, "readwrite") suite.TestOpenIDSTSWithRolePolicy(c, testRoleARNs[0], testRoleMap[testRoleARNs[0]]) suite.TestOpenIDServiceAccWithRolePolicy(c) suite.TearDownSuite(c) }, ) } } func TestIAMWithOpenIDWithRolePolicyWithPolicyVariablesServerSuite(t *testing.T) { for i, testCase := range iamTestSuites { t.Run( fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription), func(t *testing.T) { c := &check{t, testCase.serverType} suite := testCase openIDServer := os.Getenv(EnvTestOpenIDServer) if openIDServer == "" { c.Skip("Skipping OpenID test as no OpenID server is provided.") } suite.SetUpSuite(c) suite.SetUpOpenID(c, openIDServer, "projecta,projectb,projectaorb") suite.TestOpenIDSTSWithRolePolicyWithPolVar(c, testRoleARNs[0], testRoleMap[testRoleARNs[0]]) suite.TearDownSuite(c) }, ) } } const ( testRoleARN = "arn:minio:iam:::role/nOybJqMNzNmroqEKq5D0EUsRZw0" testRoleARN2 = "arn:minio:iam:::role/domXb70kze7Ugc1SaxaeFchhLP4" ) var ( testRoleARNs = []string{testRoleARN, testRoleARN2} // Load test client app and test role mapping depending on test // environment. testClientApps, testRoleMap = func() ([]OpenIDClientAppParams, map[string]OpenIDClientAppParams) { var apps []OpenIDClientAppParams m := map[string]OpenIDClientAppParams{} openIDServer := os.Getenv(EnvTestOpenIDServer) if openIDServer != "" { apps = append(apps, OpenIDClientAppParams{ ClientID: "minio-client-app", ClientSecret: "minio-client-app-secret", ProviderURL: openIDServer, RedirectURL: "http://127.0.0.1:10000/oauth_callback", }) m[testRoleARNs[len(apps)-1]] = apps[len(apps)-1] } openIDServer2 := os.Getenv(EnvTestOpenIDServer2) if openIDServer2 != "" { apps = append(apps, OpenIDClientAppParams{ ClientID: "minio-client-app-2", ClientSecret: "minio-client-app-secret-2", ProviderURL: openIDServer2, RedirectURL: "http://127.0.0.1:10000/oauth_callback", }) m[testRoleARNs[len(apps)-1]] = apps[len(apps)-1] } return apps, m }() ) func (s *TestSuiteIAM) TestOpenIDSTSWithRolePolicy(c *check, roleARN string, clientApp OpenIDClientAppParams) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Generate web identity JWT by interacting with OpenID IDP. token, err := MockOpenIDTestUserInteraction(ctx, clientApp, "dillon@example.io", "dillon") if err != nil { c.Fatalf("mock user err: %v", err) } // Generate STS credential. webID := cr.STSWebIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, GetWebIDTokenExpiry: func() (*cr.WebIdentityToken, error) { return &cr.WebIdentityToken{ Token: token, }, nil }, RoleARN: roleARN, } value, err := webID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } // fmt.Printf("value: %#v\n", value) minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) } func (s *TestSuiteIAM) TestOpenIDServiceAccWithRolePolicy(c *check) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Generate web identity STS token by interacting with OpenID IDP. token, err := MockOpenIDTestUserInteraction(ctx, testAppParams, "dillon@example.io", "dillon") if err != nil { c.Fatalf("mock user err: %v", err) } webID := cr.STSWebIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, GetWebIDTokenExpiry: func() (*cr.WebIdentityToken, error) { return &cr.WebIdentityToken{ Token: token, }, nil }, RoleARN: testRoleARN, } value, err := webID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } // Create an madmin client with user creds userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, }) if err != nil { c.Fatalf("Err creating user admin client: %v", err) } userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport) // Create svc acc cr := c.mustCreateSvcAccount(ctx, value.AccessKeyID, userAdmClient) // 1. Check that svc account appears in listing c.assertSvcAccAppearsInListing(ctx, userAdmClient, value.AccessKeyID, cr.AccessKey) // 2. Check that svc account info can be queried c.assertSvcAccInfoQueryable(ctx, userAdmClient, value.AccessKeyID, cr.AccessKey, true) // 3. Check S3 access c.assertSvcAccS3Access(ctx, s, cr, bucket) // 5. Check that service account can be deleted. c.assertSvcAccDeletion(ctx, s, userAdmClient, value.AccessKeyID, bucket) } // Constants for Policy Variables test. var ( policyProjectA = `{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetBucketLocation", "s3:ListAllMyBuckets" ], "Resource": "arn:aws:s3:::*" }, { "Effect": "Allow", "Action": "s3:*", "Resource": [ "arn:aws:s3:::projecta", "arn:aws:s3:::projecta/*" ], "Condition": { "ForAnyValue:StringEquals": { "jwt:groups": [ "projecta" ] } } } ] } ` policyProjectB = `{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetBucketLocation", "s3:ListAllMyBuckets" ], "Resource": "arn:aws:s3:::*" }, { "Effect": "Allow", "Action": "s3:*", "Resource": [ "arn:aws:s3:::projectb", "arn:aws:s3:::projectb/*" ], "Condition": { "ForAnyValue:StringEquals": { "jwt:groups": [ "projectb" ] } } } ] } ` policyProjectAorB = `{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetBucketLocation", "s3:ListAllMyBuckets" ], "Resource": "arn:aws:s3:::*" }, { "Effect": "Allow", "Action": "s3:*", "Resource": [ "arn:aws:s3:::projectaorb", "arn:aws:s3:::projectaorb/*" ], "Condition": { "ForAnyValue:StringEquals": { "jwt:groups": [ "projecta", "projectb" ] } } } ] }` policyProjectsMap = map[string]string{ // grants access to bucket `projecta` if user is in group `projecta` "projecta": policyProjectA, // grants access to bucket `projectb` if user is in group `projectb` "projectb": policyProjectB, // grants access to bucket `projectaorb` if user is in either group // `projecta` or `projectb` "projectaorb": policyProjectAorB, } ) func (s *TestSuiteIAM) TestOpenIDSTSWithRolePolicyWithPolVar(c *check, roleARN string, clientApp OpenIDClientAppParams) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Create project buckets buckets := []string{"projecta", "projectb", "projectaorb", "other"} for _, bucket := range buckets { err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } } // Create policies for polName, polContent := range policyProjectsMap { err := s.adm.AddCannedPolicy(ctx, polName, []byte(polContent)) if err != nil { c.Fatalf("policy add error: %v", err) } } makeSTSClient := func(user, password string) *minio.Client { // Generate web identity JWT by interacting with OpenID IDP. token, err := MockOpenIDTestUserInteraction(ctx, clientApp, user, password) if err != nil { c.Fatalf("mock user err: %v", err) } // Generate STS credential. webID := cr.STSWebIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, GetWebIDTokenExpiry: func() (*cr.WebIdentityToken, error) { return &cr.WebIdentityToken{ Token: token, }, nil }, RoleARN: roleARN, } value, err := webID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } // fmt.Printf("value: %#v\n", value) minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } return minioClient } // user dillon's groups attribute is ["projecta", "projectb"] dillonClient := makeSTSClient("dillon@example.io", "dillon") // Validate client's permissions c.mustListBuckets(ctx, dillonClient) c.mustListObjects(ctx, dillonClient, "projecta") c.mustListObjects(ctx, dillonClient, "projectb") c.mustListObjects(ctx, dillonClient, "projectaorb") c.mustNotListObjects(ctx, dillonClient, "other") // this user's groups attribute is ["projectb"] lisaClient := makeSTSClient("ejones@example.io", "liza") // Validate client's permissions c.mustListBuckets(ctx, lisaClient) c.mustNotListObjects(ctx, lisaClient, "projecta") c.mustListObjects(ctx, lisaClient, "projectb") c.mustListObjects(ctx, lisaClient, "projectaorb") c.mustNotListObjects(ctx, lisaClient, "other") } func TestIAMWithOpenIDMultipleConfigsValidation1(t *testing.T) { openIDServer := os.Getenv(EnvTestOpenIDServer) openIDServer2 := os.Getenv(EnvTestOpenIDServer2) if openIDServer == "" || openIDServer2 == "" { t.Skip("Skipping OpenID test as enough OpenID servers are not provided.") } testApps := testClientApps rolePolicies := []string{ "", // Treated as claim-based provider as no role policy is given. "readwrite", } for i, testCase := range iamTestSuites { t.Run( fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription), func(t *testing.T) { c := &check{t, testCase.serverType} suite := testCase suite.SetUpSuite(c) defer suite.TearDownSuite(c) err := suite.SetUpOpenIDs(c, testApps, rolePolicies) if err != nil { c.Fatalf("config with 1 claim based and 1 role based provider should pass but got: %v", err) } }, ) } } func TestIAMWithOpenIDMultipleConfigsValidation2(t *testing.T) { openIDServer := os.Getenv(EnvTestOpenIDServer) openIDServer2 := os.Getenv(EnvTestOpenIDServer2) if openIDServer == "" || openIDServer2 == "" { t.Skip("Skipping OpenID test as enough OpenID servers are not provided.") } testApps := testClientApps rolePolicies := []string{ "", // Treated as claim-based provider as no role policy is given. "", // Treated as claim-based provider as no role policy is given. } for i, testCase := range iamTestSuites { t.Run( fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription), func(t *testing.T) { c := &check{t, testCase.serverType} suite := testCase suite.SetUpSuite(c) defer suite.TearDownSuite(c) err := suite.SetUpOpenIDs(c, testApps, rolePolicies) if err == nil { c.Fatalf("config with 2 claim based provider should fail") } }, ) } } func TestIAMWithOpenIDWithMultipleRolesServerSuite(t *testing.T) { openIDServer := os.Getenv(EnvTestOpenIDServer) openIDServer2 := os.Getenv(EnvTestOpenIDServer2) if openIDServer == "" || openIDServer2 == "" { t.Skip("Skipping OpenID test as enough OpenID servers are not provided.") } testApps := testClientApps rolePolicies := []string{ "consoleAdmin", "readwrite", } for i, testCase := range iamTestSuites { t.Run( fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription), func(t *testing.T) { c := &check{t, testCase.serverType} suite := testCase suite.SetUpSuite(c) err := suite.SetUpOpenIDs(c, testApps, rolePolicies) if err != nil { c.Fatalf("Error setting up openid providers for tests: %v", err) } suite.TestOpenIDSTSWithRolePolicy(c, testRoleARNs[0], testRoleMap[testRoleARNs[0]]) suite.TestOpenIDSTSWithRolePolicy(c, testRoleARNs[1], testRoleMap[testRoleARNs[1]]) suite.TestOpenIDServiceAccWithRolePolicy(c) suite.TearDownSuite(c) }, ) } } // Access Management Plugin tests func TestIAM_AMPWithOpenIDWithMultipleRolesServerSuite(t *testing.T) { openIDServer := os.Getenv(EnvTestOpenIDServer) openIDServer2 := os.Getenv(EnvTestOpenIDServer2) if openIDServer == "" || openIDServer2 == "" { t.Skip("Skipping OpenID test as enough OpenID servers are not provided.") } testApps := testClientApps rolePolicies := []string{ "consoleAdmin", "readwrite", } for i, testCase := range iamTestSuites { t.Run( fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription), func(t *testing.T) { c := &check{t, testCase.serverType} suite := testCase suite.SetUpSuite(c) defer suite.TearDownSuite(c) err := suite.SetUpOpenIDs(c, testApps, rolePolicies) if err != nil { c.Fatalf("Error setting up openid providers for tests: %v", err) } suite.SetUpAccMgmtPlugin(c) suite.TestOpenIDSTSWithRolePolicyUnderAMP(c, testRoleARNs[0], testRoleMap[testRoleARNs[0]]) suite.TestOpenIDSTSWithRolePolicyUnderAMP(c, testRoleARNs[1], testRoleMap[testRoleARNs[1]]) suite.TestOpenIDServiceAccWithRolePolicyUnderAMP(c) }, ) } } func (s *TestSuiteIAM) TestOpenIDSTSWithRolePolicyUnderAMP(c *check, roleARN string, clientApp OpenIDClientAppParams) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Generate web identity JWT by interacting with OpenID IDP. token, err := MockOpenIDTestUserInteraction(ctx, clientApp, "dillon@example.io", "dillon") if err != nil { c.Fatalf("mock user err: %v", err) } // Generate STS credential. webID := cr.STSWebIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, GetWebIDTokenExpiry: func() (*cr.WebIdentityToken, error) { return &cr.WebIdentityToken{ Token: token, }, nil }, RoleARN: roleARN, } value, err := webID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } // fmt.Printf("value: %#v\n", value) minioClient, err := minio.New(s.endpoint, &minio.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, Transport: s.TestSuiteCommon.client.Transport, }) if err != nil { c.Fatalf("Error initializing client: %v", err) } // Validate that the client from sts creds can access the bucket. c.mustListObjects(ctx, minioClient, bucket) // Validate that the client from STS creds cannot upload any object as // it is denied by the plugin. c.mustNotUpload(ctx, minioClient, bucket) } func (s *TestSuiteIAM) TestOpenIDServiceAccWithRolePolicyUnderAMP(c *check) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() bucket := getRandomBucketName() err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}) if err != nil { c.Fatalf("bucket create error: %v", err) } // Generate web identity STS token by interacting with OpenID IDP. token, err := MockOpenIDTestUserInteraction(ctx, testAppParams, "dillon@example.io", "dillon") if err != nil { c.Fatalf("mock user err: %v", err) } webID := cr.STSWebIdentity{ Client: s.TestSuiteCommon.client, STSEndpoint: s.endPoint, GetWebIDTokenExpiry: func() (*cr.WebIdentityToken, error) { return &cr.WebIdentityToken{ Token: token, }, nil }, RoleARN: testRoleARN, } value, err := webID.Retrieve() if err != nil { c.Fatalf("Expected to generate STS creds, got err: %#v", err) } // Create an madmin client with user creds userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken), Secure: s.secure, }) if err != nil { c.Fatalf("Err creating user admin client: %v", err) } userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport) // Create svc acc cr := c.mustCreateSvcAccount(ctx, value.AccessKeyID, userAdmClient) // 1. Check that svc account appears in listing c.assertSvcAccAppearsInListing(ctx, userAdmClient, value.AccessKeyID, cr.AccessKey) // 2. Check that svc account info can be queried c.assertSvcAccInfoQueryable(ctx, userAdmClient, value.AccessKeyID, cr.AccessKey, true) // 3. Check S3 access c.assertSvcAccS3Access(ctx, s, cr, bucket) // 3.1 Validate that the client from STS creds cannot upload any object as // it is denied by the plugin. c.mustNotUpload(ctx, s.getUserClient(c, cr.AccessKey, cr.SecretKey, ""), bucket) // Check that session policies do not apply - as policy enforcement is // delegated to plugin. { svcAK, svcSK := mustGenerateCredentials(c) // This policy does not allow listing objects. policyBytes := []byte(fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject" ], "Resource": [ "arn:aws:s3:::%s/*" ] } ] }`, bucket)) cr, err := userAdmClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{ Policy: policyBytes, TargetUser: value.AccessKeyID, AccessKey: svcAK, SecretKey: svcSK, }) if err != nil { c.Fatalf("Unable to create svc acc: %v", err) } svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "") // Though the attached policy does not allow listing, it will be // ignored because the plugin allows it. c.mustListObjects(ctx, svcClient, bucket) } // 4. Check that service account's secret key and account status can be // updated. c.assertSvcAccSecretKeyAndStatusUpdate(ctx, s, userAdmClient, value.AccessKeyID, bucket) // 5. Check that service account can be deleted. c.assertSvcAccDeletion(ctx, s, userAdmClient, value.AccessKeyID, bucket) }