From 8ff2a7a2b94af8fc03e3e5b24b5da7d8a3c6e585 Mon Sep 17 00:00:00 2001 From: Aditya Manthramurthy Date: Fri, 5 Apr 2024 20:13:35 -0700 Subject: [PATCH] fix: IAM import/export: remove sts group handling (#19422) There are no separate STS group mappings to be handled. Also add tests for basic import/export sanity. --- cmd/admin-handlers-users.go | 63 ++------------ cmd/sts-handlers_test.go | 169 ++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 55 deletions(-) diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index b3b22d5a8..7a1b4595f 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -1756,15 +1756,14 @@ func (a adminAPIHandlers) AttachDetachPolicyBuiltin(w http.ResponseWriter, r *ht } const ( - allPoliciesFile = "policies.json" - allUsersFile = "users.json" - allGroupsFile = "groups.json" - allSvcAcctsFile = "svcaccts.json" - userPolicyMappingsFile = "user_mappings.json" - groupPolicyMappingsFile = "group_mappings.json" - stsUserPolicyMappingsFile = "stsuser_mappings.json" - stsGroupPolicyMappingsFile = "stsgroup_mappings.json" - iamAssetsDir = "iam-assets" + allPoliciesFile = "policies.json" + allUsersFile = "users.json" + allGroupsFile = "groups.json" + allSvcAcctsFile = "svcaccts.json" + userPolicyMappingsFile = "user_mappings.json" + groupPolicyMappingsFile = "group_mappings.json" + stsUserPolicyMappingsFile = "stsuser_mappings.json" + iamAssetsDir = "iam-assets" ) // ExportIAMHandler - exports all iam info as a zipped file @@ -1813,7 +1812,6 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) { userPolicyMappingsFile, groupPolicyMappingsFile, stsUserPolicyMappingsFile, - stsGroupPolicyMappingsFile, } for _, f := range iamFiles { iamFile := pathJoin(iamAssetsDir, f) @@ -1985,22 +1983,6 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } - case stsGroupPolicyMappingsFile: - groupPolicyMap := xsync.NewMapOf[string, MappedPolicy]() - err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, true, groupPolicyMap) - if err != nil { - writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) - return - } - grpPolData, err := json.Marshal(mappedPoliciesToMap(groupPolicyMap)) - if err != nil { - writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) - return - } - if err = rawDataFn(bytes.NewReader(grpPolData), iamFile, len(grpPolData)); err != nil { - writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) - return - } } } } @@ -2391,35 +2373,6 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) { } } } - - // import sts group policy mappings - { - f, err := zr.Open(pathJoin(iamAssetsDir, stsGroupPolicyMappingsFile)) - switch { - case errors.Is(err, os.ErrNotExist): - case err != nil: - writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsGroupPolicyMappingsFile, ""), r.URL) - return - default: - defer f.Close() - var grpPolicyMap map[string]MappedPolicy - data, err := io.ReadAll(f) - if err != nil { - writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsGroupPolicyMappingsFile, ""), r.URL) - return - } - if err = json.Unmarshal(data, &grpPolicyMap); err != nil { - writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, stsGroupPolicyMappingsFile, ""), r.URL) - return - } - for g, pm := range grpPolicyMap { - if _, err := globalIAMSys.PolicyDBSet(ctx, g, pm.Policies, unknownIAMUserType, true); err != nil { - writeErrorResponseJSON(ctx, w, importError(ctx, err, stsGroupPolicyMappingsFile, g), r.URL) - return - } - } - } - } } func addExpirationToCondValues(exp *time.Time, condValues map[string][]string) { diff --git a/cmd/sts-handlers_test.go b/cmd/sts-handlers_test.go index 415f8c080..3004f663b 100644 --- a/cmd/sts-handlers_test.go +++ b/cmd/sts-handlers_test.go @@ -18,9 +18,12 @@ package cmd import ( + "bytes" "context" "fmt" + "io" "os" + "reflect" "strings" "testing" "time" @@ -752,6 +755,172 @@ func TestIAMWithLDAPNonNormalizedBaseDNConfigServerSuite(t *testing.T) { } } +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) + } + }, + ) + } +} + +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) 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()