From 7640cd24c9b99ba161097bae0ab21f64fb633c4e Mon Sep 17 00:00:00 2001 From: Aditya Manthramurthy Date: Tue, 23 Apr 2024 18:23:08 -0700 Subject: [PATCH] fix: avoid some IAM import errors if LDAP enabled (#19591) When LDAP is enabled, previously we were: - rejecting creation of users and groups via the IAM import functionality - throwing a `not a valid DN` error when non-LDAP group mappings are present This change allows for these cases as we need to support situations where the MinIO server contains users, groups and policy mappings created before LDAP was enabled. --- cmd/admin-handlers-users.go | 55 +++++++++++++++++++++++++++++++++---- cmd/iam.go | 36 ++---------------------- cmd/site-replication.go | 50 +++++++++++++++++++++++++++++++-- cmd/sts-handlers_test.go | 47 ++++++++++++++++++++++++++++--- 4 files changed, 143 insertions(+), 45 deletions(-) diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index 1d4ceb7e4..8fd5fbbfb 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -271,7 +271,14 @@ func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Requ return } } - updatedAt, err = globalIAMSys.AddUsersToGroup(ctx, updReq.Group, updReq.Members) + + if globalIAMSys.LDAPConfig.Enabled() { + // We don't allow internal group manipulation in this API when LDAP + // is enabled for now. + err = errIAMActionNotAllowed + } else { + updatedAt, err = globalIAMSys.AddUsersToGroup(ctx, updReq.Group, updReq.Members) + } } if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) @@ -507,6 +514,12 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) { return } + // We don't allow internal user creation with LDAP enabled for now. + if globalIAMSys.LDAPConfig.Enabled() { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) + return + } + updatedAt, err := globalIAMSys.CreateUser(ctx, accessKey, ureq) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) @@ -1556,7 +1569,12 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request })) } -// SetPolicyForUserOrGroup - PUT /minio/admin/v3/set-policy?policy=xxx&user-or-group=?[&is-group] +// SetPolicyForUserOrGroup - sets a policy on a user or a group. +// +// PUT /minio/admin/v3/set-policy?policy=xxx&user-or-group=?[&is-group] +// +// Deprecated: This API is replaced by attach/detach policy APIs for specific +// type of users (builtin or LDAP). func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -1608,6 +1626,31 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http userType := regUser if globalIAMSys.GetUsersSysType() == LDAPUsersSysType { userType = stsUser + + // Validate that the user or group exists in LDAP and use the normalized + // form of the entityName (which will be an LDAP DN). + var err error + if isGroup { + var foundGroupDN string + if foundGroupDN, err = globalIAMSys.LDAPConfig.GetValidatedGroupDN(nil, entityName); err != nil { + iamLogIf(ctx, err) + } else if foundGroupDN == "" { + err = errNoSuchGroup + } + entityName = foundGroupDN + } else { + var foundUserDN string + if foundUserDN, err = globalIAMSys.LDAPConfig.GetValidatedDNForUsername(entityName); err != nil { + iamLogIf(ctx, err) + } else if foundUserDN == "" { + err = errNoSuchUser + } + entityName = foundUserDN + } + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } } updatedAt, err := globalIAMSys.PolicyDBSet(ctx, entityName, policyName, userType, isGroup) @@ -2157,12 +2200,12 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) { // If group does not exist, then check if the group has beginning and end space characters // we will reject such group names. if errors.Is(gerr, errNoSuchGroup) && hasSpaceBE(group) { - writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, err, allGroupsFile, group), r.URL) + writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, gerr, allGroupsFile, group), r.URL) return } } if _, gerr := globalIAMSys.AddUsersToGroup(ctx, group, grpInfo.Members); gerr != nil { - writeErrorResponseJSON(ctx, w, importError(ctx, err, allGroupsFile, group), r.URL) + writeErrorResponseJSON(ctx, w, importError(ctx, gerr, allGroupsFile, group), r.URL) return } } @@ -2209,7 +2252,8 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) { return } } - // service account access key cannot have space characters beginning and end of the string. + // service account access key cannot have space characters + // beginning and end of the string. if hasSpaceBE(svcAcctReq.AccessKey) { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL) return @@ -2384,6 +2428,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) { writeErrorResponseJSON(ctx, w, importError(ctx, errIAMActionNotAllowed, stsUserPolicyMappingsFile, u), r.URL) return } + if _, err := globalIAMSys.PolicyDBSet(ctx, u, pm.Policies, stsUser, false); err != nil { writeErrorResponseJSON(ctx, w, importError(ctx, err, stsUserPolicyMappingsFile, u), r.URL) return diff --git a/cmd/iam.go b/cmd/iam.go index bcaba8236..223595394 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -1269,10 +1269,6 @@ func (sys *IAMSys) CreateUser(ctx context.Context, accessKey string, ureq madmin return updatedAt, errServerNotInitialized } - if sys.usersSysType != MinIOUsersSysType { - return updatedAt, errIAMActionNotAllowed - } - if !auth.IsAccessKeyValid(accessKey) { return updatedAt, auth.ErrInvalidAccessKeyLength } @@ -1702,10 +1698,6 @@ func (sys *IAMSys) AddUsersToGroup(ctx context.Context, group string, members [] return updatedAt, errServerNotInitialized } - if sys.usersSysType != MinIOUsersSysType { - return updatedAt, errIAMActionNotAllowed - } - updatedAt, err = sys.store.AddUsersToGroup(ctx, group, members) if err != nil { return updatedAt, err @@ -1777,36 +1769,14 @@ func (sys *IAMSys) ListGroups(ctx context.Context) (r []string, err error) { } } -// PolicyDBSet - sets a policy for a user or group in the PolicyDB - the user doesn't have to exist since sometimes they are virtuals +// PolicyDBSet - sets a policy for a user or group in the PolicyDB. This does +// not validate if the user/group exists - that is the responsibility of the +// caller. func (sys *IAMSys) PolicyDBSet(ctx context.Context, name, policy string, userType IAMUserType, isGroup bool) (updatedAt time.Time, err error) { if !sys.Initialized() { return updatedAt, errServerNotInitialized } - if sys.LDAPConfig.Enabled() { - if isGroup { - var foundGroupDN string - if foundGroupDN, err = sys.LDAPConfig.GetValidatedGroupDN(nil, name); err != nil { - iamLogIf(ctx, err) - return - } else if foundGroupDN == "" { - err = errNoSuchGroup - return - } - name = foundGroupDN - } else { - var foundUserDN string - if foundUserDN, err = sys.LDAPConfig.GetValidatedDNForUsername(name); err != nil { - iamLogIf(ctx, err) - return - } else if foundUserDN == "" { - err = errNoSuchUser - return - } - name = foundUserDN - } - } - updatedAt, err = sys.store.PolicyDBSet(ctx, name, policy, userType, isGroup) if err != nil { return diff --git a/cmd/site-replication.go b/cmd/site-replication.go index 8b96b1914..814898075 100644 --- a/cmd/site-replication.go +++ b/cmd/site-replication.go @@ -1282,7 +1282,13 @@ func (c *SiteReplicationSys) PeerIAMUserChangeHandler(ctx context.Context, chang // only changing the account status. _, err = globalIAMSys.SetUserStatus(ctx, change.AccessKey, userReq.Status) } else { - _, err = globalIAMSys.CreateUser(ctx, change.AccessKey, userReq) + // We don't allow internal user creation with LDAP enabled for now + // (both sites must have LDAP disabled). + if globalIAMSys.LDAPConfig.Enabled() { + err = errIAMActionNotAllowed + } else { + _, err = globalIAMSys.CreateUser(ctx, change.AccessKey, userReq) + } } } if err != nil { @@ -1312,7 +1318,13 @@ func (c *SiteReplicationSys) PeerGroupInfoChangeHandler(ctx context.Context, cha if updReq.Status != "" && len(updReq.Members) == 0 { _, err = globalIAMSys.SetGroupStatus(ctx, updReq.Group, updReq.Status == madmin.GroupEnabled) } else { - _, err = globalIAMSys.AddUsersToGroup(ctx, updReq.Group, updReq.Members) + if globalIAMSys.LDAPConfig.Enabled() { + // We don't allow internal group manipulation in this API when + // LDAP is enabled for now (both sites must have LDAP disabled). + err = errIAMActionNotAllowed + } else { + _, err = globalIAMSys.AddUsersToGroup(ctx, updReq.Group, updReq.Members) + } if err == nil && updReq.Status != madmin.GroupEnabled { _, err = globalIAMSys.SetGroupStatus(ctx, updReq.Group, updReq.Status == madmin.GroupEnabled) } @@ -1417,7 +1429,39 @@ func (c *SiteReplicationSys) PeerPolicyMappingHandler(ctx context.Context, mappi } } - _, err := globalIAMSys.PolicyDBSet(ctx, mapping.UserOrGroup, mapping.Policy, IAMUserType(mapping.UserType), mapping.IsGroup) + // When LDAP is enabled, we verify that the user or group exists in LDAP and + // use the normalized form of the entityName (which will be an LDAP DN). + userType := IAMUserType(mapping.UserType) + isGroup := mapping.IsGroup + entityName := mapping.UserOrGroup + if globalIAMSys.GetUsersSysType() == LDAPUsersSysType && userType == stsUser { + + // Validate that the user or group exists in LDAP and use the normalized + // form of the entityName (which will be an LDAP DN). + var err error + if isGroup { + var foundGroupDN string + if foundGroupDN, err = globalIAMSys.LDAPConfig.GetValidatedGroupDN(nil, entityName); err != nil { + iamLogIf(ctx, err) + } else if foundGroupDN == "" { + err = errNoSuchGroup + } + entityName = foundGroupDN + } else { + var foundUserDN string + if foundUserDN, err = globalIAMSys.LDAPConfig.GetValidatedDNForUsername(entityName); err != nil { + iamLogIf(ctx, err) + } else if foundUserDN == "" { + err = errNoSuchUser + } + entityName = foundUserDN + } + if err != nil { + return wrapSRErr(err) + } + } + + _, err := globalIAMSys.PolicyDBSet(ctx, entityName, mapping.Policy, userType, isGroup) if err != nil { return wrapSRErr(err) } diff --git a/cmd/sts-handlers_test.go b/cmd/sts-handlers_test.go index 55bcf9968..e4429cbdd 100644 --- a/cmd/sts-handlers_test.go +++ b/cmd/sts-handlers_test.go @@ -806,8 +806,29 @@ func TestIAMImportAssetWithLDAP(t *testing.T) { 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:::*"]}]}}`, - allUsersFile: `{}`, - allGroupsFile: `{}`, + + // 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" + } +} +`, allSvcAcctsFile: `{ "u4ccRswj62HV3Ifwima7": { "parent": "uid=svc.algorithm,OU=swengg,DC=min,DC=io", @@ -828,14 +849,32 @@ func TestIAMImportAssetWithLDAP(t *testing.T) { } } `, - userPolicyMappingsFile: `{}`, - // Contains duplicate mapping with same policy, we should not error out. + // 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",