diff --git a/cmd/admin-handler-utils.go b/cmd/admin-handler-utils.go index 6142eaeaa..e7c80c164 100644 --- a/cmd/admin-handler-utils.go +++ b/cmd/admin-handler-utils.go @@ -103,6 +103,12 @@ func toAdminAPIErr(ctx context.Context, err error) APIError { Description: err.Error(), HTTPStatusCode: http.StatusServiceUnavailable, } + case errors.Is(err, errPolicyInUse): + apiErr = APIError{ + Code: "XMinioAdminPolicyInUse", + Description: "The policy cannot be removed, as it is in use", + HTTPStatusCode: http.StatusBadRequest, + } case errors.Is(err, kes.ErrKeyExists): apiErr = APIError{ Code: "XMinioKMSKeyExists", diff --git a/cmd/admin-handlers-users_test.go b/cmd/admin-handlers-users_test.go index 634acfe34..6baccb94c 100644 --- a/cmd/admin-handlers-users_test.go +++ b/cmd/admin-handlers-users_test.go @@ -258,10 +258,20 @@ func (s *TestSuiteIAM) TestPolicyCreate(c *check) { c.Fatalf("policy was missing!") } - // 5. Check that policy can be deleted. + // 5. Check that policy cannot be deleted when attached to a user. + err = s.adm.RemoveCannedPolicy(ctx, policy) + if err == nil { + c.Fatalf("policy could be unexpectedly deleted!") + } + + // 6. Delete the user and then delete the policy. + err = s.adm.RemoveUser(ctx, accessKey) + if err != nil { + c.Fatalf("user could not be deleted: %v", err) + } err = s.adm.RemoveCannedPolicy(ctx, policy) if err != nil { - c.Fatalf("policy delete err: %v", err) + c.Fatalf("policy del err: %v", err) } } @@ -627,7 +637,8 @@ func (c *check) mustListObjects(ctx context.Context, client *minio.Client, bucke res := client.ListObjects(ctx, bucket, minio.ListObjectsOptions{}) v, ok := <-res if ok && v.Err != nil { - c.Fatalf("user was unable to list unexpectedly!") + msg := fmt.Sprintf("user was unable to list: %v", v.Err) + c.Fatalf(msg) } } diff --git a/cmd/iam-dummy-store.go b/cmd/iam-dummy-store.go index df69058f3..f4ebbd1fc 100644 --- a/cmd/iam-dummy-store.go +++ b/cmd/iam-dummy-store.go @@ -27,22 +27,37 @@ import ( type iamDummyStore struct { sync.RWMutex + *iamCache + usersSysType UsersSysType } -func (ids *iamDummyStore) lock() { +func newIAMDummyStore(usersSysType UsersSysType) *iamDummyStore { + return &iamDummyStore{ + iamCache: newIamCache(), + usersSysType: usersSysType, + } +} + +func (ids *iamDummyStore) rlock() *iamCache { + ids.RLock() + return ids.iamCache +} + +func (ids *iamDummyStore) runlock() { + ids.RUnlock() +} + +func (ids *iamDummyStore) lock() *iamCache { ids.Lock() + return ids.iamCache } func (ids *iamDummyStore) unlock() { ids.Unlock() } -func (ids *iamDummyStore) rlock() { - ids.RLock() -} - -func (ids *iamDummyStore) runlock() { - ids.RUnlock() +func (ids *iamDummyStore) getUsersSysType() UsersSysType { + return ids.usersSysType } func (ids *iamDummyStore) migrateBackendFormat(context.Context) error { diff --git a/cmd/iam-etcd-store.go b/cmd/iam-etcd-store.go index e5db91178..ff79bd799 100644 --- a/cmd/iam-etcd-store.go +++ b/cmd/iam-etcd-store.go @@ -62,27 +62,37 @@ func extractPathPrefixAndSuffix(s string, prefix string, suffix string) string { type IAMEtcdStore struct { sync.RWMutex + *iamCache + + usersSysType UsersSysType + client *etcd.Client } -func newIAMEtcdStore(client *etcd.Client) *IAMEtcdStore { - return &IAMEtcdStore{client: client} +func newIAMEtcdStore(client *etcd.Client, usersSysType UsersSysType) *IAMEtcdStore { + return &IAMEtcdStore{client: client, usersSysType: usersSysType} } -func (ies *IAMEtcdStore) lock() { +func (ies *IAMEtcdStore) rlock() *iamCache { + ies.RLock() + return ies.iamCache +} + +func (ies *IAMEtcdStore) runlock() { + ies.RUnlock() +} + +func (ies *IAMEtcdStore) lock() *iamCache { ies.Lock() + return ies.iamCache } func (ies *IAMEtcdStore) unlock() { ies.Unlock() } -func (ies *IAMEtcdStore) rlock() { - ies.RLock() -} - -func (ies *IAMEtcdStore) runlock() { - ies.RUnlock() +func (ies *IAMEtcdStore) getUsersSysType() UsersSysType { + return ies.usersSysType } func (ies *IAMEtcdStore) saveIAMConfig(ctx context.Context, item interface{}, itemPath string, opts ...options) error { @@ -244,6 +254,8 @@ func (ies *IAMEtcdStore) migrateToV1(ctx context.Context) error { // Should be called under config migration lock func (ies *IAMEtcdStore) migrateBackendFormat(ctx context.Context) error { + ies.Lock() + defer ies.Unlock() return ies.migrateToV1(ctx) } @@ -260,7 +272,7 @@ func (ies *IAMEtcdStore) loadPolicyDoc(ctx context.Context, policy string, m map return nil } -func (ies *IAMEtcdStore) getPolicyDoc(ctx context.Context, kvs *mvccpb.KeyValue, m map[string]iampolicy.Policy) error { +func (ies *IAMEtcdStore) getPolicyDocKV(ctx context.Context, kvs *mvccpb.KeyValue, m map[string]iampolicy.Policy) error { var p iampolicy.Policy err := getIAMConfig(&p, kvs.Value, string(kvs.Key)) if err != nil { @@ -286,14 +298,14 @@ func (ies *IAMEtcdStore) loadPolicyDocs(ctx context.Context, m map[string]iampol // Parse all values to construct the policies data model. for _, kvs := range r.Kvs { - if err = ies.getPolicyDoc(ctx, kvs, m); err != nil && err != errNoSuchPolicy { + if err = ies.getPolicyDocKV(ctx, kvs, m); err != nil && err != errNoSuchPolicy { return err } } return nil } -func (ies *IAMEtcdStore) getUser(ctx context.Context, userkv *mvccpb.KeyValue, userType IAMUserType, m map[string]auth.Credentials, basePrefix string) error { +func (ies *IAMEtcdStore) getUserKV(ctx context.Context, userkv *mvccpb.KeyValue, userType IAMUserType, m map[string]auth.Credentials, basePrefix string) error { var u UserIdentity err := getIAMConfig(&u, userkv.Value, string(userkv.Key)) if err != nil { @@ -355,7 +367,7 @@ func (ies *IAMEtcdStore) loadUsers(ctx context.Context, userType IAMUserType, m // Parse all users values to create the proper data model for _, userKv := range r.Kvs { - if err = ies.getUser(ctx, userKv, userType, m, basePrefix); err != nil && err != errNoSuchUser { + if err = ies.getUserKV(ctx, userKv, userType, m, basePrefix); err != nil && err != errNoSuchUser { return err } } diff --git a/cmd/iam-object-store.go b/cmd/iam-object-store.go index 1143e3d08..075c2c057 100644 --- a/cmd/iam-object-store.go +++ b/cmd/iam-object-store.go @@ -34,30 +34,44 @@ import ( // IAMObjectStore implements IAMStorageAPI type IAMObjectStore struct { - // Protect assignment to objAPI + // Protect access to storage within the current server. sync.RWMutex + *iamCache + + usersSysType UsersSysType + objAPI ObjectLayer } -func newIAMObjectStore(objAPI ObjectLayer) *IAMObjectStore { - return &IAMObjectStore{objAPI: objAPI} +func newIAMObjectStore(objAPI ObjectLayer, usersSysType UsersSysType) *IAMObjectStore { + return &IAMObjectStore{ + iamCache: newIamCache(), + objAPI: objAPI, + usersSysType: usersSysType, + } } -func (iamOS *IAMObjectStore) lock() { +func (iamOS *IAMObjectStore) rlock() *iamCache { + iamOS.RLock() + return iamOS.iamCache +} + +func (iamOS *IAMObjectStore) runlock() { + iamOS.RUnlock() +} + +func (iamOS *IAMObjectStore) lock() *iamCache { iamOS.Lock() + return iamOS.iamCache } func (iamOS *IAMObjectStore) unlock() { iamOS.Unlock() } -func (iamOS *IAMObjectStore) rlock() { - iamOS.RLock() -} - -func (iamOS *IAMObjectStore) runlock() { - iamOS.RUnlock() +func (iamOS *IAMObjectStore) getUsersSysType() UsersSysType { + return iamOS.usersSysType } // Migrate users directory in a single scan. @@ -182,6 +196,8 @@ func (iamOS *IAMObjectStore) migrateToV1(ctx context.Context) error { // Should be called under config migration lock func (iamOS *IAMObjectStore) migrateBackendFormat(ctx context.Context) error { + iamOS.Lock() + defer iamOS.Unlock() return iamOS.migrateToV1(ctx) } diff --git a/cmd/iam-store.go b/cmd/iam-store.go new file mode 100644 index 000000000..0e67f178a --- /dev/null +++ b/cmd/iam-store.go @@ -0,0 +1,1712 @@ +// 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 ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/dustin/go-humanize" + "github.com/minio/madmin-go" + "github.com/minio/minio-go/v7/pkg/set" + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/logger" + iampolicy "github.com/minio/pkg/iam/policy" +) + +const ( + // IAM configuration directory. + iamConfigPrefix = minioConfigPrefix + "/iam" + + // IAM users directory. + iamConfigUsersPrefix = iamConfigPrefix + "/users/" + + // IAM service accounts directory. + iamConfigServiceAccountsPrefix = iamConfigPrefix + "/service-accounts/" + + // IAM groups directory. + iamConfigGroupsPrefix = iamConfigPrefix + "/groups/" + + // IAM policies directory. + iamConfigPoliciesPrefix = iamConfigPrefix + "/policies/" + + // IAM sts directory. + iamConfigSTSPrefix = iamConfigPrefix + "/sts/" + + // IAM Policy DB prefixes. + iamConfigPolicyDBPrefix = iamConfigPrefix + "/policydb/" + iamConfigPolicyDBUsersPrefix = iamConfigPolicyDBPrefix + "users/" + iamConfigPolicyDBSTSUsersPrefix = iamConfigPolicyDBPrefix + "sts-users/" + iamConfigPolicyDBServiceAccountsPrefix = iamConfigPolicyDBPrefix + "service-accounts/" + iamConfigPolicyDBGroupsPrefix = iamConfigPolicyDBPrefix + "groups/" + + // IAM identity file which captures identity credentials. + iamIdentityFile = "identity.json" + + // IAM policy file which provides policies for each users. + iamPolicyFile = "policy.json" + + // IAM group members file + iamGroupMembersFile = "members.json" + + // IAM format file + iamFormatFile = "format.json" + + iamFormatVersion1 = 1 +) + +type iamFormat struct { + Version int `json:"version"` +} + +func newIAMFormatVersion1() iamFormat { + return iamFormat{Version: iamFormatVersion1} +} + +func getIAMFormatFilePath() string { + return iamConfigPrefix + SlashSeparator + iamFormatFile +} + +func getUserIdentityPath(user string, userType IAMUserType) string { + var basePath string + switch userType { + case svcUser: + basePath = iamConfigServiceAccountsPrefix + case stsUser: + basePath = iamConfigSTSPrefix + default: + basePath = iamConfigUsersPrefix + } + return pathJoin(basePath, user, iamIdentityFile) +} + +func getGroupInfoPath(group string) string { + return pathJoin(iamConfigGroupsPrefix, group, iamGroupMembersFile) +} + +func getPolicyDocPath(name string) string { + return pathJoin(iamConfigPoliciesPrefix, name, iamPolicyFile) +} + +func getMappedPolicyPath(name string, userType IAMUserType, isGroup bool) string { + if isGroup { + return pathJoin(iamConfigPolicyDBGroupsPrefix, name+".json") + } + switch userType { + case svcUser: + return pathJoin(iamConfigPolicyDBServiceAccountsPrefix, name+".json") + case stsUser: + return pathJoin(iamConfigPolicyDBSTSUsersPrefix, name+".json") + default: + return pathJoin(iamConfigPolicyDBUsersPrefix, name+".json") + } +} + +// UserIdentity represents a user's secret key and their status +type UserIdentity struct { + Version int `json:"version"` + Credentials auth.Credentials `json:"credentials"` +} + +func newUserIdentity(cred auth.Credentials) UserIdentity { + return UserIdentity{Version: 1, Credentials: cred} +} + +// GroupInfo contains info about a group +type GroupInfo struct { + Version int `json:"version"` + Status string `json:"status"` + Members []string `json:"members"` +} + +func newGroupInfo(members []string) GroupInfo { + return GroupInfo{Version: 1, Status: statusEnabled, Members: members} +} + +// MappedPolicy represents a policy name mapped to a user or group +type MappedPolicy struct { + Version int `json:"version"` + Policies string `json:"policy"` +} + +// converts a mapped policy into a slice of distinct policies +func (mp MappedPolicy) toSlice() []string { + var policies []string + for _, policy := range strings.Split(mp.Policies, ",") { + policy = strings.TrimSpace(policy) + if policy == "" { + continue + } + policies = append(policies, policy) + } + return policies +} + +func (mp MappedPolicy) policySet() set.StringSet { + var policies []string + for _, policy := range strings.Split(mp.Policies, ",") { + policy = strings.TrimSpace(policy) + if policy == "" { + continue + } + policies = append(policies, policy) + } + return set.CreateStringSet(policies...) +} + +func newMappedPolicy(policy string) MappedPolicy { + return MappedPolicy{Version: 1, Policies: policy} +} + +// key options +type options struct { + ttl int64 //expiry in seconds +} + +type iamWatchEvent struct { + isCreated bool // !isCreated implies a delete event. + keyPath string +} + +// iamCache contains in-memory cache of IAM data. +type iamCache struct { + // map of policy names to policy definitions + iamPolicyDocsMap map[string]iampolicy.Policy + // map of usernames to credentials + iamUsersMap map[string]auth.Credentials + // map of group names to group info + iamGroupsMap map[string]GroupInfo + // map of user names to groups they are a member of + iamUserGroupMemberships map[string]set.StringSet + // map of usernames/temporary access keys to policy names + iamUserPolicyMap map[string]MappedPolicy + // map of group names to policy names + iamGroupPolicyMap map[string]MappedPolicy +} + +func newIamCache() *iamCache { + return &iamCache{ + iamPolicyDocsMap: map[string]iampolicy.Policy{}, + iamUsersMap: map[string]auth.Credentials{}, + iamGroupsMap: map[string]GroupInfo{}, + iamUserGroupMemberships: map[string]set.StringSet{}, + iamUserPolicyMap: map[string]MappedPolicy{}, + iamGroupPolicyMap: map[string]MappedPolicy{}, + } +} + +// buildUserGroupMemberships - builds the memberships map. IMPORTANT: +// Assumes that c.Lock is held by caller. +func (c *iamCache) buildUserGroupMemberships() { + for group, gi := range c.iamGroupsMap { + c.updateGroupMembershipsMap(group, &gi) + } +} + +// updateGroupMembershipsMap - updates the memberships map for a +// group. IMPORTANT: Assumes c.Lock() is held by caller. +func (c *iamCache) updateGroupMembershipsMap(group string, gi *GroupInfo) { + if gi == nil { + return + } + for _, member := range gi.Members { + v := c.iamUserGroupMemberships[member] + if v == nil { + v = set.CreateStringSet(group) + } else { + v.Add(group) + } + c.iamUserGroupMemberships[member] = v + } +} + +// removeGroupFromMembershipsMap - removes the group from every member +// in the cache. IMPORTANT: Assumes c.Lock() is held by caller. +func (c *iamCache) removeGroupFromMembershipsMap(group string) { + for member, groups := range c.iamUserGroupMemberships { + if !groups.Contains(group) { + continue + } + groups.Remove(group) + c.iamUserGroupMemberships[member] = groups + } +} + +// policyDBGet - lower-level helper; does not take locks. +// +// If a group is passed, it returns policies associated with the group. +// +// If a user is passed, it returns policies of the user along with any groups +// that the server knows the user is a member of. +// +// In LDAP users mode, the server does not store any group membership +// information in IAM (i.e sys.iam*Map) - this info is stored only in the STS +// generated credentials. Thus we skip looking up group memberships, user map, +// and group map and check the appropriate policy maps directly. +func (c *iamCache) policyDBGet(mode UsersSysType, name string, isGroup bool) ([]string, error) { + if isGroup { + if mode == MinIOUsersSysType { + g, ok := c.iamGroupsMap[name] + if !ok { + return nil, errNoSuchGroup + } + + // Group is disabled, so we return no policy - this + // ensures the request is denied. + if g.Status == statusDisabled { + return nil, nil + } + } + + return c.iamGroupPolicyMap[name].toSlice(), nil + } + + if name == globalActiveCred.AccessKey { + return []string{"consoleAdmin"}, nil + } + + // When looking for a user's policies, we also check if the user + // and the groups they are member of are enabled. + var parentName string + u, ok := c.iamUsersMap[name] + if ok { + if !u.IsValid() { + return nil, nil + } + parentName = u.ParentUser + } + + mp, ok := c.iamUserPolicyMap[name] + if !ok { + // Service accounts with root credentials, inherit parent permissions + if parentName == globalActiveCred.AccessKey && u.IsServiceAccount() { + // even if this is set, the claims present in the service + // accounts apply the final permissions if any. + return []string{"consoleAdmin"}, nil + } + if parentName != "" { + mp = c.iamUserPolicyMap[parentName] + } + } + + // returned policy could be empty + policies := mp.toSlice() + + for _, group := range c.iamUserGroupMemberships[name].ToSlice() { + // Skip missing or disabled groups + gi, ok := c.iamGroupsMap[group] + if !ok || gi.Status == statusDisabled { + continue + } + + policies = append(policies, c.iamGroupPolicyMap[group].toSlice()...) + } + + return policies, nil +} + +// IAMStorageAPI defines an interface for the IAM persistence layer +type IAMStorageAPI interface { + + // The role of the read-write lock is to prevent go routines from + // concurrently reading and writing the IAM storage. The (r)lock() + // functions return the iamCache. The cache can be safely written to + // only when returned by `lock()`. + lock() *iamCache + unlock() + rlock() *iamCache + runlock() + + migrateBackendFormat(context.Context) error + + getUsersSysType() UsersSysType + + loadPolicyDoc(ctx context.Context, policy string, m map[string]iampolicy.Policy) error + loadPolicyDocs(ctx context.Context, m map[string]iampolicy.Policy) error + + loadUser(ctx context.Context, user string, userType IAMUserType, m map[string]auth.Credentials) error + loadUsers(ctx context.Context, userType IAMUserType, m map[string]auth.Credentials) error + + loadGroup(ctx context.Context, group string, m map[string]GroupInfo) error + loadGroups(ctx context.Context, m map[string]GroupInfo) error + + loadMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, m map[string]MappedPolicy) error + loadMappedPolicies(ctx context.Context, userType IAMUserType, isGroup bool, m map[string]MappedPolicy) error + + saveIAMConfig(ctx context.Context, item interface{}, path string, opts ...options) error + loadIAMConfig(ctx context.Context, item interface{}, path string) error + deleteIAMConfig(ctx context.Context, path string) error + + savePolicyDoc(ctx context.Context, policyName string, p iampolicy.Policy) error + saveMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, mp MappedPolicy, opts ...options) error + saveUserIdentity(ctx context.Context, name string, userType IAMUserType, u UserIdentity, opts ...options) error + saveGroupInfo(ctx context.Context, group string, gi GroupInfo) error + + deletePolicyDoc(ctx context.Context, policyName string) error + deleteMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool) error + deleteUserIdentity(ctx context.Context, name string, userType IAMUserType) error + deleteGroupInfo(ctx context.Context, name string) error +} + +// iamStorageWatcher is implemented by `IAMStorageAPI` implementers that +// additionally support watching storage for changes. +type iamStorageWatcher interface { + watch(ctx context.Context, keyPath string) <-chan iamWatchEvent +} + +// LoadIAMCache reads all IAM items and populates a new iamCache object and +// replaces the in-memory cache object. +func (store *IAMStoreSys) LoadIAMCache(ctx context.Context) error { + newCache := newIamCache() + + // Since we are only reading storage here, we do not need a write lock. + cache := store.lock() + defer store.unlock() + + if err := store.loadPolicyDocs(ctx, newCache.iamPolicyDocsMap); err != nil { + return err + } + + // Sets default canned policies, if none are set. + setDefaultCannedPolicies(newCache.iamPolicyDocsMap) + + if store.getUsersSysType() == MinIOUsersSysType { + if err := store.loadUsers(ctx, regUser, newCache.iamUsersMap); err != nil { + return err + } + if err := store.loadGroups(ctx, newCache.iamGroupsMap); err != nil { + return err + } + } + + // load polices mapped to users + if err := store.loadMappedPolicies(ctx, regUser, false, newCache.iamUserPolicyMap); err != nil { + return err + } + + // load policies mapped to groups + if err := store.loadMappedPolicies(ctx, regUser, true, newCache.iamGroupPolicyMap); err != nil { + return err + } + + // load service accounts + if err := store.loadUsers(ctx, svcUser, newCache.iamUsersMap); err != nil { + return err + } + + // load STS temp users + if err := store.loadUsers(ctx, stsUser, newCache.iamUsersMap); err != nil { + return err + } + + // load STS policy mappings + if err := store.loadMappedPolicies(ctx, stsUser, false, newCache.iamUserPolicyMap); err != nil { + return err + } + + newCache.buildUserGroupMemberships() + + cache.iamGroupPolicyMap = newCache.iamGroupPolicyMap + cache.iamGroupsMap = newCache.iamGroupsMap + cache.iamPolicyDocsMap = newCache.iamPolicyDocsMap + cache.iamUserGroupMemberships = newCache.iamUserGroupMemberships + cache.iamUserPolicyMap = newCache.iamUserPolicyMap + cache.iamUsersMap = newCache.iamUsersMap + + return nil +} + +// IAMStoreSys contains IAMStorageAPI to add higher-level methods on the storage +// layer. +type IAMStoreSys struct { + IAMStorageAPI +} + +// HasWatcher - returns if the storage system has a watcher. +func (store *IAMStoreSys) HasWatcher() bool { + _, ok := store.IAMStorageAPI.(iamStorageWatcher) + return ok +} + +// GetUser - fetches credential from memory. +func (store *IAMStoreSys) GetUser(user string) (auth.Credentials, bool) { + cache := store.rlock() + defer store.runlock() + + c, ok := cache.iamUsersMap[user] + return c, ok +} + +// GetMappedPolicy - fetches mapped policy from memory. +func (store *IAMStoreSys) GetMappedPolicy(name string, isGroup bool) (MappedPolicy, bool) { + cache := store.rlock() + defer store.runlock() + + if isGroup { + v, ok := cache.iamGroupPolicyMap[name] + return v, ok + } + + v, ok := cache.iamUserPolicyMap[name] + return v, ok +} + +// GroupNotificationHandler - updates in-memory cache on notification of +// change (e.g. peer notification for object storage and etcd watch +// notification). +func (store *IAMStoreSys) GroupNotificationHandler(ctx context.Context, group string) error { + cache := store.rlock() + defer store.runlock() + + err := store.loadGroup(ctx, group, cache.iamGroupsMap) + if err != nil && err != errNoSuchGroup { + return err + } + + if err == errNoSuchGroup { + // group does not exist - so remove from memory. + cache.removeGroupFromMembershipsMap(group) + delete(cache.iamGroupsMap, group) + delete(cache.iamGroupPolicyMap, group) + return nil + } + + gi := cache.iamGroupsMap[group] + + // Updating the group memberships cache happens in two steps: + // + // 1. Remove the group from each user's list of memberships. + // 2. Add the group to each member's list of memberships. + // + // This ensures that regardless of members being added or + // removed, the cache stays current. + cache.removeGroupFromMembershipsMap(group) + cache.updateGroupMembershipsMap(group, &gi) + return nil +} + +// PolicyDBGet - fetches policies associated with the given user or group, and +// additional groups if provided. +func (store *IAMStoreSys) PolicyDBGet(name string, isGroup bool, groups ...string) ([]string, error) { + if name == "" { + return nil, errInvalidArgument + } + + cache := store.rlock() + defer store.runlock() + + policies, err := cache.policyDBGet(store.getUsersSysType(), name, isGroup) + if err != nil { + return nil, err + } + + if !isGroup { + for _, group := range groups { + ps, err := cache.policyDBGet(store.getUsersSysType(), group, true) + if err != nil { + return nil, err + } + policies = append(policies, ps...) + } + } + + return policies, nil +} + +// AddUsersToGroup - adds users to group, creating the group if needed. +func (store *IAMStoreSys) AddUsersToGroup(ctx context.Context, group string, members []string) error { + if group == "" { + return errInvalidArgument + } + + cache := store.lock() + defer store.unlock() + + // Validate that all members exist. + for _, member := range members { + cr, ok := cache.iamUsersMap[member] + if !ok { + return errNoSuchUser + } + if cr.IsTemp() || cr.IsServiceAccount() { + return errIAMActionNotAllowed + } + } + + gi, ok := cache.iamGroupsMap[group] + if !ok { + // Set group as enabled by default when it doesn't + // exist. + gi = newGroupInfo(members) + } else { + mergedMembers := append(gi.Members, members...) + uniqMembers := set.CreateStringSet(mergedMembers...).ToSlice() + gi.Members = uniqMembers + } + + if err := store.saveGroupInfo(ctx, group, gi); err != nil { + return err + } + + cache.iamGroupsMap[group] = gi + + // update user-group membership map + for _, member := range members { + gset := cache.iamUserGroupMemberships[member] + if gset == nil { + gset = set.CreateStringSet(group) + } else { + gset.Add(group) + } + cache.iamUserGroupMemberships[member] = gset + } + + return nil + +} + +// helper function - does not take any locks. Updates only cache if +// updateCacheOnly is set. +func removeMembersFromGroup(ctx context.Context, store *IAMStoreSys, cache *iamCache, group string, members []string, updateCacheOnly bool) error { + gi, ok := cache.iamGroupsMap[group] + if !ok { + return errNoSuchGroup + } + + s := set.CreateStringSet(gi.Members...) + d := set.CreateStringSet(members...) + gi.Members = s.Difference(d).ToSlice() + + if !updateCacheOnly { + err := store.saveGroupInfo(ctx, group, gi) + if err != nil { + return err + } + } + cache.iamGroupsMap[group] = gi + + // update user-group membership map + for _, member := range members { + gset := cache.iamUserGroupMemberships[member] + if gset == nil { + continue + } + gset.Remove(group) + cache.iamUserGroupMemberships[member] = gset + } + + return nil +} + +// RemoveUsersFromGroup - removes users from group, deleting it if it is empty. +func (store *IAMStoreSys) RemoveUsersFromGroup(ctx context.Context, group string, members []string) error { + if group == "" { + return errInvalidArgument + } + + cache := store.lock() + defer store.unlock() + + // Validate that all members exist. + for _, member := range members { + cr, ok := cache.iamUsersMap[member] + if !ok { + return errNoSuchUser + } + if cr.IsTemp() || cr.IsServiceAccount() { + return errIAMActionNotAllowed + } + } + + gi, ok := cache.iamGroupsMap[group] + if !ok { + return errNoSuchGroup + } + + // Check if attempting to delete a non-empty group. + if len(members) == 0 && len(gi.Members) != 0 { + return errGroupNotEmpty + } + + if len(members) == 0 { + // len(gi.Members) == 0 here. + + // Remove the group from storage. First delete the + // mapped policy. No-mapped-policy case is ignored. + if err := store.deleteMappedPolicy(ctx, group, regUser, true); err != nil && err != errNoSuchPolicy { + return err + } + if err := store.deleteGroupInfo(ctx, group); err != nil && err != errNoSuchGroup { + return err + } + + // Delete from server memory + delete(cache.iamGroupsMap, group) + delete(cache.iamGroupPolicyMap, group) + return nil + } + + return removeMembersFromGroup(ctx, store, cache, group, members, false) +} + +// SetGroupStatus - updates group status +func (store *IAMStoreSys) SetGroupStatus(ctx context.Context, group string, enabled bool) error { + if group == "" { + return errInvalidArgument + } + + cache := store.lock() + defer store.unlock() + + gi, ok := cache.iamGroupsMap[group] + if !ok { + return errNoSuchGroup + } + + if enabled { + gi.Status = statusEnabled + } else { + gi.Status = statusDisabled + } + + if err := store.saveGroupInfo(ctx, group, gi); err != nil { + return err + } + cache.iamGroupsMap[group] = gi + return nil +} + +// GetGroupDescription - builds up group description +func (store *IAMStoreSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err error) { + cache := store.rlock() + defer store.runlock() + + ps, err := cache.policyDBGet(store.getUsersSysType(), group, true) + if err != nil { + return gd, err + } + + policy := strings.Join(ps, ",") + + if store.getUsersSysType() != MinIOUsersSysType { + return madmin.GroupDesc{ + Name: group, + Policy: policy, + }, nil + } + + gi, ok := cache.iamGroupsMap[group] + if !ok { + return gd, errNoSuchGroup + } + + return madmin.GroupDesc{ + Name: group, + Status: gi.Status, + Members: gi.Members, + Policy: policy, + }, nil +} + +// ListGroups - lists groups. Since this is not going to be a frequent +// operation, we fetch this info from storage, and refresh the cache as well. +func (store *IAMStoreSys) ListGroups(ctx context.Context) (res []string, err error) { + cache := store.rlock() + defer store.runlock() + + if store.getUsersSysType() == MinIOUsersSysType { + m := map[string]GroupInfo{} + err = store.loadGroups(ctx, m) + if err != nil { + return + } + cache.iamGroupsMap = m + + for k := range cache.iamGroupsMap { + res = append(res, k) + } + } + + if store.getUsersSysType() == LDAPUsersSysType { + m := map[string]MappedPolicy{} + err = store.loadMappedPolicies(ctx, stsUser, true, m) + if err != nil { + return + } + cache.iamGroupPolicyMap = m + for k := range cache.iamGroupPolicyMap { + res = append(res, k) + } + } + + return +} + +// PolicyDBSet - update the policy mapping for the given user or group in +// storage and in cache. +func (store *IAMStoreSys) PolicyDBSet(ctx context.Context, name, policy string, userType IAMUserType, isGroup bool) error { + if name == "" { + return errInvalidArgument + } + + cache := store.lock() + defer store.unlock() + + // Validate that user and group exist. + if store.getUsersSysType() == MinIOUsersSysType { + if !isGroup { + if _, ok := cache.iamUsersMap[name]; !ok { + return errNoSuchUser + } + } else { + if _, ok := cache.iamGroupsMap[name]; !ok { + return errNoSuchGroup + } + } + } + + // Handle policy mapping removal. + if policy == "" { + if store.getUsersSysType() == LDAPUsersSysType { + // Add a fallback removal towards previous content that may come back + // as a ghost user due to lack of delete, this change occurred + // introduced in PR #11840 + store.deleteMappedPolicy(ctx, name, regUser, false) + } + err := store.deleteMappedPolicy(ctx, name, userType, isGroup) + if err != nil && err != errNoSuchPolicy { + return err + } + if !isGroup { + delete(cache.iamUserPolicyMap, name) + } else { + delete(cache.iamGroupPolicyMap, name) + } + return nil + } + + // Handle policy mapping set/update + mp := newMappedPolicy(policy) + for _, p := range mp.toSlice() { + if _, found := cache.iamPolicyDocsMap[policy]; !found { + logger.LogIf(GlobalContext, fmt.Errorf("%w: (%s)", errNoSuchPolicy, p)) + return errNoSuchPolicy + } + } + + if err := store.saveMappedPolicy(ctx, name, userType, isGroup, mp); err != nil { + return err + } + if !isGroup { + cache.iamUserPolicyMap[name] = mp + } else { + cache.iamGroupPolicyMap[name] = mp + } + return nil + +} + +// PolicyNotificationHandler - loads given policy from storage. If not present, +// deletes from cache. This notification only reads from storage, and updates +// cache. When the notification is for a policy deletion, it updates the +// user-policy and group-policy maps as well. +func (store *IAMStoreSys) PolicyNotificationHandler(ctx context.Context, policy string) error { + if policy == "" { + return errInvalidArgument + } + + cache := store.rlock() + defer store.runlock() + + err := store.loadPolicyDoc(ctx, policy, cache.iamPolicyDocsMap) + if err == errNoSuchPolicy { + // policy was deleted, update cache. + delete(cache.iamPolicyDocsMap, policy) + + // update user policy map + for u, mp := range cache.iamUserPolicyMap { + pset := mp.policySet() + if !pset.Contains(policy) { + continue + } + _, ok := cache.iamUsersMap[u] + if !ok { + // happens when account is deleted or + // expired. + delete(cache.iamUserPolicyMap, u) + continue + } + pset.Remove(policy) + cache.iamUserPolicyMap[u] = newMappedPolicy(strings.Join(pset.ToSlice(), ",")) + } + + // update group policy map + for g, mp := range cache.iamGroupPolicyMap { + pset := mp.policySet() + if !pset.Contains(policy) { + continue + } + pset.Remove(policy) + cache.iamGroupPolicyMap[g] = newMappedPolicy(strings.Join(pset.ToSlice(), ",")) + } + + return nil + } + return err +} + +// DeletePolicy - deletes policy from storage and cache. +func (store *IAMStoreSys) DeletePolicy(ctx context.Context, policy string) error { + if policy == "" { + return errInvalidArgument + } + + cache := store.lock() + defer store.unlock() + + // Check if policy is mapped to any existing user or group. + users := []string{} + groups := []string{} + for u, mp := range cache.iamUserPolicyMap { + pset := mp.policySet() + if _, ok := cache.iamUsersMap[u]; !ok { + // This case can happen when a temporary account is + // deleted or expired - remove it from userPolicyMap. + delete(cache.iamUserPolicyMap, u) + continue + } + if pset.Contains(policy) { + users = append(users, u) + } + } + for g, mp := range cache.iamGroupPolicyMap { + pset := mp.policySet() + if pset.Contains(policy) { + groups = append(groups, g) + } + } + if len(users) != 0 || len(groups) != 0 { + // error out when a policy could not be deleted as it was in use. + loggedErr := fmt.Errorf("policy could not be deleted as it is use (users=%s; groups=%s)", + fmt.Sprintf("[%s]", strings.Join(users, ",")), + fmt.Sprintf("[%s]", strings.Join(groups, ",")), + ) + logger.LogIf(GlobalContext, loggedErr) + return errPolicyInUse + } + + err := store.deletePolicyDoc(ctx, policy) + if err == errNoSuchPolicy { + // Ignore error if policy is already deleted. + err = nil + } + if err != nil { + return err + } + + delete(cache.iamPolicyDocsMap, policy) + return nil +} + +// GetPolicy - gets the policy definition. Allows specifying multiple comma +// separated policies - returns a combined policy. +func (store *IAMStoreSys) GetPolicy(name string) (iampolicy.Policy, error) { + if name == "" { + return iampolicy.Policy{}, errInvalidArgument + } + + cache := store.rlock() + defer store.runlock() + + policies := newMappedPolicy(name).toSlice() + var combinedPolicy iampolicy.Policy + for _, policy := range policies { + if policy == "" { + continue + } + v, ok := cache.iamPolicyDocsMap[policy] + if !ok { + return v, errNoSuchPolicy + } + combinedPolicy = combinedPolicy.Merge(v) + } + return combinedPolicy, nil +} + +// SetPolicy - creates a policy with name. +func (store *IAMStoreSys) SetPolicy(ctx context.Context, name string, policy iampolicy.Policy) error { + + if policy.IsEmpty() || name == "" { + return errInvalidArgument + } + + cache := store.lock() + defer store.unlock() + + if err := store.savePolicyDoc(ctx, name, policy); err != nil { + return err + } + + cache.iamPolicyDocsMap[name] = policy + return nil + +} + +// ListPolicies - fetches all policies from storage and updates cache as well. +// If bucketName is non-empty, returns policies matching the bucket. +func (store *IAMStoreSys) ListPolicies(ctx context.Context, bucketName string) (map[string]iampolicy.Policy, error) { + cache := store.rlock() + defer store.runlock() + + m := map[string]iampolicy.Policy{} + err := store.loadPolicyDocs(ctx, m) + if err != nil { + return nil, err + } + + cache.iamPolicyDocsMap = m + + ret := map[string]iampolicy.Policy{} + for k, v := range m { + if bucketName == "" || v.MatchResource(bucketName) { + ret[k] = v + } + } + + return ret, nil +} + +// helper function - does not take locks. +func filterPolicies(cache *iamCache, policyName string, bucketName string) (string, iampolicy.Policy) { + var policies []string + mp := newMappedPolicy(policyName) + combinedPolicy := iampolicy.Policy{} + for _, policy := range mp.toSlice() { + if policy == "" { + continue + } + p, found := cache.iamPolicyDocsMap[policy] + if found { + if bucketName == "" || p.MatchResource(bucketName) { + policies = append(policies, policy) + combinedPolicy = combinedPolicy.Merge(p) + } + } + } + return strings.Join(policies, ","), combinedPolicy +} + +// FilterPolicies - accepts a comma separated list of policy names as a string +// and bucket and returns only policies that currently exist in MinIO. If +// bucketName is non-empty, additionally filters policies matching the bucket. +// The first returned value is the list of currently existing policies, and the +// second is their combined policy definition. +func (store *IAMStoreSys) FilterPolicies(policyName string, bucketName string) (string, iampolicy.Policy) { + cache := store.rlock() + defer store.runlock() + + return filterPolicies(cache, policyName, bucketName) + +} + +// GetBucketUsers - returns users (not STS or service accounts) that have access +// to the bucket. User is included even if a group policy that grants access to +// the bucket is disabled. +func (store *IAMStoreSys) GetBucketUsers(bucket string) (map[string]madmin.UserInfo, error) { + if bucket == "" { + return nil, errInvalidArgument + } + + cache := store.rlock() + defer store.runlock() + + result := map[string]madmin.UserInfo{} + for k, v := range cache.iamUsersMap { + if v.IsTemp() || v.IsServiceAccount() { + continue + } + var policies []string + mp, ok := cache.iamUserPolicyMap[k] + if ok { + policies = append(policies, mp.Policies) + for _, group := range cache.iamUserGroupMemberships[k].ToSlice() { + if nmp, ok := cache.iamGroupPolicyMap[group]; ok { + policies = append(policies, nmp.Policies) + } + } + } + matchedPolicies, _ := filterPolicies(cache, strings.Join(policies, ","), bucket) + if len(matchedPolicies) > 0 { + result[k] = madmin.UserInfo{ + PolicyName: matchedPolicies, + Status: func() madmin.AccountStatus { + if v.IsValid() { + return madmin.AccountEnabled + } + return madmin.AccountDisabled + }(), + MemberOf: cache.iamUserGroupMemberships[k].ToSlice(), + } + } + } + + return result, nil +} + +// GetUsers - returns all users (not STS or service accounts). +func (store *IAMStoreSys) GetUsers() map[string]madmin.UserInfo { + cache := store.rlock() + defer store.runlock() + + result := map[string]madmin.UserInfo{} + for k, v := range cache.iamUsersMap { + if v.IsTemp() || v.IsServiceAccount() { + continue + } + result[k] = madmin.UserInfo{ + PolicyName: cache.iamUserPolicyMap[k].Policies, + Status: func() madmin.AccountStatus { + if v.IsValid() { + return madmin.AccountEnabled + } + return madmin.AccountDisabled + }(), + MemberOf: cache.iamUserGroupMemberships[k].ToSlice(), + } + } + + if store.getUsersSysType() == LDAPUsersSysType { + for k, v := range cache.iamUserPolicyMap { + result[k] = madmin.UserInfo{ + PolicyName: v.Policies, + Status: madmin.AccountEnabled, + } + } + } + + return result +} + +// GetUserInfo - get info on a user. +func (store *IAMStoreSys) GetUserInfo(name string) (u madmin.UserInfo, err error) { + if name == "" { + return u, errInvalidArgument + } + + cache := store.rlock() + defer store.runlock() + + if store.getUsersSysType() != MinIOUsersSysType { + // If the user has a mapped policy or is a member of a group, we + // return that info. Otherwise we return error. + var groups []string + for _, v := range cache.iamUsersMap { + if v.ParentUser == name { + groups = v.Groups + break + } + } + mappedPolicy, ok := cache.iamUserPolicyMap[name] + if !ok { + return u, errNoSuchUser + } + return madmin.UserInfo{ + PolicyName: mappedPolicy.Policies, + MemberOf: groups, + }, nil + } + + cred, found := cache.iamUsersMap[name] + if !found { + return u, errNoSuchUser + } + + if cred.IsTemp() || cred.IsServiceAccount() { + return u, errIAMActionNotAllowed + } + + return madmin.UserInfo{ + PolicyName: cache.iamUserPolicyMap[name].Policies, + Status: func() madmin.AccountStatus { + if cred.IsValid() { + return madmin.AccountEnabled + } + return madmin.AccountDisabled + }(), + MemberOf: cache.iamUserGroupMemberships[name].ToSlice(), + }, nil +} + +// PolicyMappingNotificationHandler - handles updating a policy mapping from storage. +func (store *IAMStoreSys) PolicyMappingNotificationHandler(ctx context.Context, userOrGroup string, isGroup bool, userType IAMUserType) error { + if userOrGroup == "" { + return errInvalidArgument + } + + cache := store.rlock() + defer store.runlock() + + m := cache.iamGroupPolicyMap + if !isGroup { + m = cache.iamUserPolicyMap + } + err := store.loadMappedPolicy(ctx, userOrGroup, userType, isGroup, m) + if err == errNoSuchPolicy { + // This means that the policy mapping was deleted, so we update + // the cache. + delete(m, userOrGroup) + err = nil + } + return err +} + +// UserNotificationHandler - handles updating a user/STS account/service account +// from storage. +func (store *IAMStoreSys) UserNotificationHandler(ctx context.Context, accessKey string, userType IAMUserType) error { + if accessKey == "" { + return errInvalidArgument + } + + cache := store.rlock() + defer store.runlock() + + err := store.loadUser(ctx, accessKey, userType, cache.iamUsersMap) + if err == errNoSuchUser { + // User was deleted - we update the cache. + delete(cache.iamUsersMap, accessKey) + + // 1. Start with updating user-group memberships + if store.getUsersSysType() == MinIOUsersSysType { + memberOf := cache.iamUserGroupMemberships[accessKey].ToSlice() + for _, group := range memberOf { + removeErr := removeMembersFromGroup(ctx, store, cache, group, []string{accessKey}, true) + if removeErr == errNoSuchGroup { + removeErr = nil + } + if removeErr != nil { + return removeErr + } + } + } + + // 2. Remove any derived credentials from memory + if userType == regUser { + for _, u := range cache.iamUsersMap { + if u.IsServiceAccount() && u.ParentUser == accessKey { + delete(cache.iamUsersMap, u.AccessKey) + } + if u.IsTemp() && u.ParentUser == accessKey { + delete(cache.iamUsersMap, u.AccessKey) + } + } + } + + // 3. Delete any mapped policy + delete(cache.iamUserPolicyMap, accessKey) + return nil + } + if err != nil { + return err + } + if userType != svcUser { + err = store.loadMappedPolicy(ctx, accessKey, userType, false, cache.iamUserPolicyMap) + // Ignore policy not mapped error + if err != nil && err != errNoSuchPolicy { + return err + } + } + + // We are on purpose not persisting the policy map for parent + // user, although this is a hack, it is a good enough hack + // at this point in time - we need to overhaul our OIDC + // usage with service accounts with a more cleaner implementation + // + // This mapping is necessary to ensure that valid credentials + // have necessary ParentUser present - this is mainly for only + // webIdentity based STS tokens. + cred, ok := cache.iamUsersMap[accessKey] + if ok { + if cred.IsTemp() && cred.ParentUser != "" && cred.ParentUser != globalActiveCred.AccessKey { + if _, ok := cache.iamUserPolicyMap[cred.ParentUser]; !ok { + cache.iamUserPolicyMap[cred.ParentUser] = cache.iamUserPolicyMap[accessKey] + } + } + } + + return nil +} + +// DeleteUser - deletes a user from storage and cache. This only used with +// long-term users and service accounts, not STS. +func (store *IAMStoreSys) DeleteUser(ctx context.Context, accessKey string, userType IAMUserType) error { + if accessKey == "" { + return errInvalidArgument + } + + cache := store.lock() + defer store.unlock() + + // first we remove the user from their groups. + if store.getUsersSysType() == MinIOUsersSysType && userType == regUser { + memberOf := cache.iamUserGroupMemberships[accessKey].ToSlice() + for _, group := range memberOf { + removeErr := removeMembersFromGroup(ctx, store, cache, group, []string{accessKey}, false) + if removeErr != nil { + return removeErr + } + } + } + + // Now we can remove the user from memory and IAM store + + // Delete any STS and service account derived from this credential + // first. + if userType == regUser { + for _, u := range cache.iamUsersMap { + if u.IsServiceAccount() && u.ParentUser == accessKey { + _ = store.deleteUserIdentity(ctx, u.AccessKey, svcUser) + delete(cache.iamUsersMap, u.AccessKey) + } + // Delete any associated STS users. + if u.IsTemp() && u.ParentUser == accessKey { + _ = store.deleteUserIdentity(ctx, u.AccessKey, stsUser) + delete(cache.iamUsersMap, u.AccessKey) + } + } + } + + // It is ok to ignore deletion error on the mapped policy + store.deleteMappedPolicy(ctx, accessKey, userType, false) + delete(cache.iamUserPolicyMap, accessKey) + + err := store.deleteUserIdentity(ctx, accessKey, userType) + if err == errNoSuchUser { + // ignore if user is already deleted. + err = nil + } + delete(cache.iamUsersMap, accessKey) + + return err +} + +// SetTempUser - saves temporary credential to storage and cache. +func (store *IAMStoreSys) SetTempUser(ctx context.Context, accessKey string, cred auth.Credentials, policyName string) error { + if accessKey == "" || !cred.IsTemp() || cred.IsExpired() { + return errInvalidArgument + } + + ttl := int64(cred.Expiration.Sub(UTCNow()).Seconds()) + + cache := store.lock() + defer store.unlock() + + if policyName != "" { + mp := newMappedPolicy(policyName) + _, combinedPolicyStmt := filterPolicies(cache, mp.Policies, "") + + if combinedPolicyStmt.IsEmpty() { + return fmt.Errorf("specified policy %s, not found %w", policyName, errNoSuchPolicy) + } + + err := store.saveMappedPolicy(ctx, accessKey, stsUser, false, mp, options{ttl: ttl}) + if err != nil { + return err + } + + cache.iamUserPolicyMap[accessKey] = mp + + // We are on purpose not persisting the policy map for parent + // user, although this is a hack, it is a good enough hack + // at this point in time - we need to overhaul our OIDC + // usage with service accounts with a more cleaner implementation + // + // This mapping is necessary to ensure that valid credentials + // have necessary ParentUser present - this is mainly for only + // webIdentity based STS tokens. + if cred.ParentUser != "" && cred.ParentUser != globalActiveCred.AccessKey { + if _, ok := cache.iamUserPolicyMap[cred.ParentUser]; !ok { + cache.iamUserPolicyMap[cred.ParentUser] = mp + } + } + } + + u := newUserIdentity(cred) + err := store.saveUserIdentity(context.Background(), accessKey, stsUser, u, options{ttl: ttl}) + if err != nil { + return err + } + + cache.iamUsersMap[accessKey] = cred + return nil +} + +// DeleteUsers - given a set of users or access keys, deletes them along with +// any derived credentials (STS or service accounts) and any associated policy +// mappings. +func (store *IAMStoreSys) DeleteUsers(ctx context.Context, users []string) error { + cache := store.lock() + defer store.unlock() + + usersToDelete := set.CreateStringSet(users...) + for user, cred := range cache.iamUsersMap { + userType := regUser + if cred.IsServiceAccount() { + userType = svcUser + } else if cred.IsTemp() { + userType = stsUser + } + + if usersToDelete.Contains(user) || usersToDelete.Contains(cred.ParentUser) { + // Delete this user account and its policy mapping + store.deleteMappedPolicy(ctx, user, userType, false) + delete(cache.iamUserPolicyMap, user) + + // we are only logging errors, not handling them. + err := store.deleteUserIdentity(ctx, user, userType) + logger.LogIf(GlobalContext, err) + delete(cache.iamUsersMap, user) + } + } + + return nil +} + +// GetAllParentUsers - returns all distinct "parent-users" associated with STS or service +// credentials. +func (store *IAMStoreSys) GetAllParentUsers() []string { + cache := store.rlock() + defer store.runlock() + + res := set.NewStringSet() + for _, cred := range cache.iamUsersMap { + if cred.IsServiceAccount() || cred.IsTemp() { + res.Add(cred.ParentUser) + } + } + + return res.ToSlice() +} + +// SetUserStatus - sets current user status. +func (store *IAMStoreSys) SetUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) error { + if accessKey != "" && status != madmin.AccountEnabled && status != madmin.AccountDisabled { + return errInvalidArgument + } + + cache := store.lock() + defer store.unlock() + + cred, ok := cache.iamUsersMap[accessKey] + if !ok { + return errNoSuchUser + } + + if cred.IsTemp() || cred.IsServiceAccount() { + return errIAMActionNotAllowed + } + + uinfo := newUserIdentity(auth.Credentials{ + AccessKey: accessKey, + SecretKey: cred.SecretKey, + Status: func() string { + if status == madmin.AccountEnabled { + return auth.AccountOn + } + return auth.AccountOff + }(), + }) + + if err := store.saveUserIdentity(ctx, accessKey, regUser, uinfo); err != nil { + return err + } + + cache.iamUsersMap[accessKey] = uinfo.Credentials + return nil +} + +// AddServiceAccount - add a new service account +func (store *IAMStoreSys) AddServiceAccount(ctx context.Context, cred auth.Credentials) error { + cache := store.lock() + defer store.unlock() + + accessKey := cred.AccessKey + parentUser := cred.ParentUser + + // Found newly requested service account, to be an existing account - + // reject such operation (updates to the service account are handled in + // a different API). + if _, found := cache.iamUsersMap[accessKey]; found { + return errIAMActionNotAllowed + } + + // Parent user must not be a service account. + if cr, found := cache.iamUsersMap[parentUser]; found && cr.IsServiceAccount() { + return errIAMActionNotAllowed + } + + // Check that at least one policy is available. + policies, err := cache.policyDBGet(store.getUsersSysType(), parentUser, false) + if err != nil { + return err + } + for _, group := range cred.Groups { + gp, err := cache.policyDBGet(store.getUsersSysType(), group, true) + if err != nil && err != errNoSuchGroup { + return err + } + policies = append(policies, gp...) + } + if len(policies) == 0 { + return errNoSuchUser + } + + u := newUserIdentity(cred) + err = store.saveUserIdentity(ctx, u.Credentials.AccessKey, svcUser, u) + if err != nil { + return err + } + + cache.iamUsersMap[u.Credentials.AccessKey] = u.Credentials + return nil +} + +// UpdateServiceAccount - updates a service account on storage. +func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey string, opts updateServiceAccountOpts) error { + cache := store.lock() + defer store.unlock() + + cr, ok := cache.iamUsersMap[accessKey] + if !ok || !cr.IsServiceAccount() { + return errNoSuchServiceAccount + } + + if opts.secretKey != "" { + if !auth.IsSecretKeyValid(opts.secretKey) { + return auth.ErrInvalidSecretKeyLength + } + cr.SecretKey = opts.secretKey + } + + switch opts.status { + // The caller did not ask to update status account, do nothing + case "": + // Update account status + case auth.AccountOn, auth.AccountOff: + cr.Status = opts.status + default: + return errors.New("unknown account status value") + } + + if opts.sessionPolicy != nil { + m := make(map[string]interface{}) + err := opts.sessionPolicy.Validate() + if err != nil { + return err + } + policyBuf, err := json.Marshal(opts.sessionPolicy) + if err != nil { + return err + } + if len(policyBuf) > 16*humanize.KiByte { + return fmt.Errorf("Session policy should not exceed 16 KiB characters") + } + + m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf) + m[iamPolicyClaimNameSA()] = "embedded-policy" + m[parentClaim] = cr.ParentUser + cr.SessionToken, err = auth.JWTSignWithAccessKey(accessKey, m, globalActiveCred.SecretKey) + if err != nil { + return err + } + } + + u := newUserIdentity(cr) + if err := store.saveUserIdentity(ctx, u.Credentials.AccessKey, svcUser, u); err != nil { + return err + } + + cache.iamUsersMap[u.Credentials.AccessKey] = u.Credentials + + return nil +} + +// ListServiceAccounts - lists only service accounts from the cache. +func (store *IAMStoreSys) ListServiceAccounts(ctx context.Context, accessKey string) ([]auth.Credentials, error) { + cache := store.rlock() + defer store.runlock() + + var serviceAccounts []auth.Credentials + for _, v := range cache.iamUsersMap { + if v.IsServiceAccount() && v.ParentUser == accessKey { + // Hide secret key & session key here + v.SecretKey = "" + v.SessionToken = "" + serviceAccounts = append(serviceAccounts, v) + } + } + + return serviceAccounts, nil +} + +// AddUser - adds/updates long term user account to storage. +func (store *IAMStoreSys) AddUser(ctx context.Context, accessKey string, uinfo madmin.UserInfo) error { + cache := store.lock() + defer store.unlock() + + cr, ok := cache.iamUsersMap[accessKey] + + // It is not possible to update an STS account. + if ok && cr.IsTemp() { + return errIAMActionNotAllowed + } + + u := newUserIdentity(auth.Credentials{ + AccessKey: accessKey, + SecretKey: uinfo.SecretKey, + Status: func() string { + if uinfo.Status == madmin.AccountEnabled { + return auth.AccountOn + } + return auth.AccountOff + }(), + }) + + if err := store.saveUserIdentity(ctx, accessKey, regUser, u); err != nil { + return err + } + + cache.iamUsersMap[accessKey] = u.Credentials + + // Set policy if specified. + if uinfo.PolicyName != "" { + policy := uinfo.PolicyName + // Handle policy mapping set/update + mp := newMappedPolicy(policy) + for _, p := range mp.toSlice() { + if _, found := cache.iamPolicyDocsMap[policy]; !found { + logger.LogIf(GlobalContext, fmt.Errorf("%w: (%s)", errNoSuchPolicy, p)) + return errNoSuchPolicy + } + } + + if err := store.saveMappedPolicy(ctx, accessKey, regUser, false, mp); err != nil { + return err + } + cache.iamUserPolicyMap[accessKey] = mp + } + return nil + +} + +// UpdateUserSecretKey - sets user secret key to storage. +func (store *IAMStoreSys) UpdateUserSecretKey(ctx context.Context, accessKey, secretKey string) error { + cache := store.lock() + defer store.unlock() + + cred, ok := cache.iamUsersMap[accessKey] + if !ok { + return errNoSuchUser + } + + cred.SecretKey = secretKey + u := newUserIdentity(cred) + if err := store.saveUserIdentity(ctx, accessKey, regUser, u); err != nil { + return err + } + + cache.iamUsersMap[accessKey] = cred + return nil +} + +// GetSTSAndServiceAccounts - returns all STS and Service account credentials. +func (store *IAMStoreSys) GetSTSAndServiceAccounts() []auth.Credentials { + cache := store.rlock() + defer store.runlock() + + var res []auth.Credentials + for _, cred := range cache.iamUsersMap { + if cred.IsTemp() || cred.IsServiceAccount() { + res = append(res, cred) + } + } + return res +} + +// UpdateUserIdentity - updates a user credential. +func (store *IAMStoreSys) UpdateUserIdentity(ctx context.Context, cred auth.Credentials) error { + cache := store.lock() + defer store.unlock() + + userType := regUser + if cred.IsServiceAccount() { + userType = svcUser + } else if cred.IsTemp() { + userType = stsUser + } + + // Overwrite the user identity here. As store should be + // atomic, it shouldn't cause any corruption. + if err := store.saveUserIdentity(ctx, cred.AccessKey, userType, newUserIdentity(cred)); err != nil { + return err + } + cache.iamUsersMap[cred.AccessKey] = cred + return nil +} + +// LoadUser - attempts to load user info from storage and updates cache. +func (store *IAMStoreSys) LoadUser(ctx context.Context, accessKey string) { + cache := store.rlock() + defer store.runlock() + + _, found := cache.iamUsersMap[accessKey] + if !found { + store.loadUser(ctx, accessKey, regUser, cache.iamUsersMap) + if _, found = cache.iamUsersMap[accessKey]; found { + // load mapped policies + store.loadMappedPolicy(ctx, accessKey, regUser, false, cache.iamUserPolicyMap) + } else { + // check for service account + store.loadUser(ctx, accessKey, svcUser, cache.iamUsersMap) + if svc, found := cache.iamUsersMap[accessKey]; found { + // Load parent user and mapped policies. + if store.getUsersSysType() == MinIOUsersSysType { + store.loadUser(ctx, svc.ParentUser, regUser, cache.iamUsersMap) + } + store.loadMappedPolicy(ctx, svc.ParentUser, regUser, false, cache.iamUserPolicyMap) + } else { + // check for STS account + store.loadUser(ctx, accessKey, stsUser, cache.iamUsersMap) + if _, found = cache.iamUsersMap[accessKey]; found { + // Load mapped policy + store.loadMappedPolicy(ctx, accessKey, stsUser, false, cache.iamUserPolicyMap) + } + } + } + } + + // Load any associated policy definitions + for _, policy := range cache.iamUserPolicyMap[accessKey].toSlice() { + if _, found = cache.iamPolicyDocsMap[policy]; !found { + store.loadPolicyDoc(ctx, policy, cache.iamPolicyDocsMap) + } + } +} diff --git a/cmd/iam.go b/cmd/iam.go index e8f2470cf..4a689f252 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -53,155 +53,11 @@ const ( LDAPUsersSysType UsersSysType = "LDAPUsersSys" ) -const ( - // IAM configuration directory. - iamConfigPrefix = minioConfigPrefix + "/iam" - - // IAM users directory. - iamConfigUsersPrefix = iamConfigPrefix + "/users/" - - // IAM service accounts directory. - iamConfigServiceAccountsPrefix = iamConfigPrefix + "/service-accounts/" - - // IAM groups directory. - iamConfigGroupsPrefix = iamConfigPrefix + "/groups/" - - // IAM policies directory. - iamConfigPoliciesPrefix = iamConfigPrefix + "/policies/" - - // IAM sts directory. - iamConfigSTSPrefix = iamConfigPrefix + "/sts/" - - // IAM Policy DB prefixes. - iamConfigPolicyDBPrefix = iamConfigPrefix + "/policydb/" - iamConfigPolicyDBUsersPrefix = iamConfigPolicyDBPrefix + "users/" - iamConfigPolicyDBSTSUsersPrefix = iamConfigPolicyDBPrefix + "sts-users/" - iamConfigPolicyDBServiceAccountsPrefix = iamConfigPolicyDBPrefix + "service-accounts/" - iamConfigPolicyDBGroupsPrefix = iamConfigPolicyDBPrefix + "groups/" - - // IAM identity file which captures identity credentials. - iamIdentityFile = "identity.json" - - // IAM policy file which provides policies for each users. - iamPolicyFile = "policy.json" - - // IAM group members file - iamGroupMembersFile = "members.json" - - // IAM format file - iamFormatFile = "format.json" - - iamFormatVersion1 = 1 -) - const ( statusEnabled = "enabled" statusDisabled = "disabled" ) -type iamFormat struct { - Version int `json:"version"` -} - -func newIAMFormatVersion1() iamFormat { - return iamFormat{Version: iamFormatVersion1} -} - -func getIAMFormatFilePath() string { - return iamConfigPrefix + SlashSeparator + iamFormatFile -} - -func getUserIdentityPath(user string, userType IAMUserType) string { - var basePath string - switch userType { - case svcUser: - basePath = iamConfigServiceAccountsPrefix - case stsUser: - basePath = iamConfigSTSPrefix - default: - basePath = iamConfigUsersPrefix - } - return pathJoin(basePath, user, iamIdentityFile) -} - -func getGroupInfoPath(group string) string { - return pathJoin(iamConfigGroupsPrefix, group, iamGroupMembersFile) -} - -func getPolicyDocPath(name string) string { - return pathJoin(iamConfigPoliciesPrefix, name, iamPolicyFile) -} - -func getMappedPolicyPath(name string, userType IAMUserType, isGroup bool) string { - if isGroup { - return pathJoin(iamConfigPolicyDBGroupsPrefix, name+".json") - } - switch userType { - case svcUser: - return pathJoin(iamConfigPolicyDBServiceAccountsPrefix, name+".json") - case stsUser: - return pathJoin(iamConfigPolicyDBSTSUsersPrefix, name+".json") - default: - return pathJoin(iamConfigPolicyDBUsersPrefix, name+".json") - } -} - -// UserIdentity represents a user's secret key and their status -type UserIdentity struct { - Version int `json:"version"` - Credentials auth.Credentials `json:"credentials"` -} - -func newUserIdentity(cred auth.Credentials) UserIdentity { - return UserIdentity{Version: 1, Credentials: cred} -} - -// GroupInfo contains info about a group -type GroupInfo struct { - Version int `json:"version"` - Status string `json:"status"` - Members []string `json:"members"` -} - -func newGroupInfo(members []string) GroupInfo { - return GroupInfo{Version: 1, Status: statusEnabled, Members: members} -} - -// MappedPolicy represents a policy name mapped to a user or group -type MappedPolicy struct { - Version int `json:"version"` - Policies string `json:"policy"` -} - -// converts a mapped policy into a slice of distinct policies -func (mp MappedPolicy) toSlice() []string { - var policies []string - for _, policy := range strings.Split(mp.Policies, ",") { - policy = strings.TrimSpace(policy) - if policy == "" { - continue - } - policies = append(policies, policy) - } - return policies -} - -func (mp MappedPolicy) policySet() set.StringSet { - var policies []string - for _, policy := range strings.Split(mp.Policies, ",") { - policy = strings.TrimSpace(policy) - if policy == "" { - continue - } - policies = append(policies, policy) - } - return set.CreateStringSet(policies...) -} - -func newMappedPolicy(policy string) MappedPolicy { - return MappedPolicy{Version: 1, Policies: policy} -} - // IAMSys - config system. type IAMSys struct { sync.Mutex @@ -210,21 +66,8 @@ type IAMSys struct { usersSysType UsersSysType - // map of policy names to policy definitions - iamPolicyDocsMap map[string]iampolicy.Policy - // map of usernames to credentials - iamUsersMap map[string]auth.Credentials - // map of group names to group info - iamGroupsMap map[string]GroupInfo - // map of user names to groups they are a member of - iamUserGroupMemberships map[string]set.StringSet - // map of usernames/temporary access keys to policy names - iamUserPolicyMap map[string]MappedPolicy - // map of group names to policy names - iamGroupPolicyMap map[string]MappedPolicy - // Persistence layer for IAM subsystem - store IAMStorageAPI + store *IAMStoreSys // configLoaded will be closed and remain so after first load. configLoaded chan struct{} @@ -239,59 +82,6 @@ const ( svcUser ) -// key options -type options struct { - ttl int64 //expiry in seconds -} - -type iamWatchEvent struct { - isCreated bool // !isCreated implies a delete event. - keyPath string -} - -// IAMStorageAPI defines an interface for the IAM persistence layer -type IAMStorageAPI interface { - lock() - unlock() - - rlock() - runlock() - - migrateBackendFormat(context.Context) error - - loadPolicyDoc(ctx context.Context, policy string, m map[string]iampolicy.Policy) error - loadPolicyDocs(ctx context.Context, m map[string]iampolicy.Policy) error - - loadUser(ctx context.Context, user string, userType IAMUserType, m map[string]auth.Credentials) error - loadUsers(ctx context.Context, userType IAMUserType, m map[string]auth.Credentials) error - - loadGroup(ctx context.Context, group string, m map[string]GroupInfo) error - loadGroups(ctx context.Context, m map[string]GroupInfo) error - - loadMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, m map[string]MappedPolicy) error - loadMappedPolicies(ctx context.Context, userType IAMUserType, isGroup bool, m map[string]MappedPolicy) error - - saveIAMConfig(ctx context.Context, item interface{}, path string, opts ...options) error - loadIAMConfig(ctx context.Context, item interface{}, path string) error - deleteIAMConfig(ctx context.Context, path string) error - - savePolicyDoc(ctx context.Context, policyName string, p iampolicy.Policy) error - saveMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, mp MappedPolicy, opts ...options) error - saveUserIdentity(ctx context.Context, name string, userType IAMUserType, u UserIdentity, opts ...options) error - saveGroupInfo(ctx context.Context, group string, gi GroupInfo) error - - deletePolicyDoc(ctx context.Context, policyName string) error - deleteMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool) error - deleteUserIdentity(ctx context.Context, name string, userType IAMUserType) error - deleteGroupInfo(ctx context.Context, name string) error -} - -// iamStorageWatcher is implemented by `IAMStorageAPI` implementers that -// additionally support watching storage for changes. -type iamStorageWatcher interface { - watch(ctx context.Context, keyPath string) <-chan iamWatchEvent -} - // LoadGroup - loads a specific group from storage, and updates the // memberships cache. If the specified group does not exist in // storage, it is removed from in-memory maps as well - this @@ -302,34 +92,7 @@ func (sys *IAMSys) LoadGroup(objAPI ObjectLayer, group string) error { return errServerNotInitialized } - sys.store.lock() - defer sys.store.unlock() - - err := sys.store.loadGroup(context.Background(), group, sys.iamGroupsMap) - if err != nil && err != errNoSuchGroup { - return err - } - - if err == errNoSuchGroup { - // group does not exist - so remove from memory. - sys.removeGroupFromMembershipsMap(group) - delete(sys.iamGroupsMap, group) - delete(sys.iamGroupPolicyMap, group) - return nil - } - - gi := sys.iamGroupsMap[group] - - // Updating the group memberships cache happens in two steps: - // - // 1. Remove the group from each user's list of memberships. - // 2. Add the group to each member's list of memberships. - // - // This ensures that regardless of members being added or - // removed, the cache stays current. - sys.removeGroupFromMembershipsMap(group) - sys.updateGroupMembershipsMap(group, &gi) - return nil + return sys.store.GroupNotificationHandler(context.Background(), group) } // LoadPolicy - reloads a specific canned policy from backend disks or etcd. @@ -338,10 +101,7 @@ func (sys *IAMSys) LoadPolicy(objAPI ObjectLayer, policyName string) error { return errServerNotInitialized } - sys.store.lock() - defer sys.store.unlock() - - return sys.store.loadPolicyDoc(context.Background(), policyName, sys.iamPolicyDocsMap) + return sys.store.PolicyNotificationHandler(context.Background(), policyName) } // LoadPolicyMapping - loads the mapped policy for a user or group @@ -351,33 +111,13 @@ func (sys *IAMSys) LoadPolicyMapping(objAPI ObjectLayer, userOrGroup string, isG return errServerNotInitialized } - sys.store.lock() - defer sys.store.unlock() - - var err error + // In case of LDAP, policy mappings are only applicable to sts users. userType := regUser if sys.usersSysType == LDAPUsersSysType { userType = stsUser } - if isGroup { - err = sys.store.loadMappedPolicy(context.Background(), userOrGroup, userType, isGroup, sys.iamGroupPolicyMap) - } else { - err = sys.store.loadMappedPolicy(context.Background(), userOrGroup, userType, isGroup, sys.iamUserPolicyMap) - } - - if err == errNoSuchPolicy { - if isGroup { - delete(sys.iamGroupPolicyMap, userOrGroup) - } else { - delete(sys.iamUserPolicyMap, userOrGroup) - } - } - // Ignore policy not mapped error - if err == errNoSuchPolicy { - err = nil - } - return err + return sys.store.PolicyMappingNotificationHandler(context.Background(), userOrGroup, isGroup, userType) } // LoadUser - reloads a specific user from backend disks or etcd. @@ -386,38 +126,7 @@ func (sys *IAMSys) LoadUser(objAPI ObjectLayer, accessKey string, userType IAMUs return errServerNotInitialized } - sys.store.lock() - defer sys.store.unlock() - - err := sys.store.loadUser(context.Background(), accessKey, userType, sys.iamUsersMap) - if err != nil { - return err - } - err = sys.store.loadMappedPolicy(context.Background(), accessKey, userType, false, sys.iamUserPolicyMap) - // Ignore policy not mapped error - if err == errNoSuchPolicy { - err = nil - } - if err != nil { - return err - } - // We are on purpose not persisting the policy map for parent - // user, although this is a hack, it is a good enough hack - // at this point in time - we need to overhaul our OIDC - // usage with service accounts with a more cleaner implementation - // - // This mapping is necessary to ensure that valid credentials - // have necessary ParentUser present - this is mainly for only - // webIdentity based STS tokens. - cred, ok := sys.iamUsersMap[accessKey] - if ok { - if cred.IsTemp() && cred.ParentUser != "" && cred.ParentUser != globalActiveCred.AccessKey { - if _, ok := sys.iamUserPolicyMap[cred.ParentUser]; !ok { - sys.iamUserPolicyMap[cred.ParentUser] = sys.iamUserPolicyMap[accessKey] - } - } - } - return nil + return sys.store.UserNotificationHandler(context.Background(), accessKey, userType) } // LoadServiceAccount - reloads a specific service account from backend disks or etcd. @@ -426,10 +135,7 @@ func (sys *IAMSys) LoadServiceAccount(accessKey string) error { return errServerNotInitialized } - sys.store.lock() - defer sys.store.unlock() - - return sys.store.loadUser(context.Background(), accessKey, svcUser, sys.iamUsersMap) + return sys.store.UserNotificationHandler(context.Background(), accessKey, svcUser) } // Perform IAM configuration migration. @@ -442,22 +148,23 @@ func (sys *IAMSys) InitStore(objAPI ObjectLayer, etcdClient *etcd.Client) { sys.Lock() defer sys.Unlock() - if etcdClient == nil { - if globalIsGateway { - sys.store = &iamDummyStore{} - } else { - sys.store = newIAMObjectStore(objAPI) - } - } else { - sys.store = newIAMEtcdStore(etcdClient) - } - if globalLDAPConfig.Enabled { sys.EnableLDAPSys() } + + if etcdClient == nil { + if globalIsGateway { + sys.store = &IAMStoreSys{newIAMDummyStore(sys.usersSysType)} + } else { + sys.store = &IAMStoreSys{newIAMObjectStore(objAPI, sys.usersSysType)} + } + } else { + sys.store = &IAMStoreSys{newIAMEtcdStore(etcdClient, sys.usersSysType)} + } + } -// Initialized check if IAM is initialized +// Initialized checks if IAM is initialized func (sys *IAMSys) Initialized() bool { if sys == nil { return false @@ -467,94 +174,13 @@ func (sys *IAMSys) Initialized() bool { return sys.store != nil } -// Load - loads all credentials +// Load - loads all credentials, policies and policy mappings. func (sys *IAMSys) Load(ctx context.Context, store IAMStorageAPI) error { - iamUsersMap := make(map[string]auth.Credentials) - iamGroupsMap := make(map[string]GroupInfo) - iamUserPolicyMap := make(map[string]MappedPolicy) - iamGroupPolicyMap := make(map[string]MappedPolicy) - iamPolicyDocsMap := make(map[string]iampolicy.Policy) - - store.lock() - defer store.unlock() - isMinIOUsersSys := sys.usersSysType == MinIOUsersSysType - - if err := store.loadPolicyDocs(ctx, iamPolicyDocsMap); err != nil { + err := sys.store.LoadIAMCache(ctx) + if err != nil { return err } - // Sets default canned policies, if none are set. - setDefaultCannedPolicies(iamPolicyDocsMap) - - if isMinIOUsersSys { - if err := store.loadUsers(ctx, regUser, iamUsersMap); err != nil { - return err - } - if err := store.loadGroups(ctx, iamGroupsMap); err != nil { - return err - } - } - - // load polices mapped to users - if err := store.loadMappedPolicies(ctx, regUser, false, iamUserPolicyMap); err != nil { - return err - } - - // load policies mapped to groups - if err := store.loadMappedPolicies(ctx, regUser, true, iamGroupPolicyMap); err != nil { - return err - } - - // load service accounts - if err := store.loadUsers(ctx, svcUser, iamUsersMap); err != nil { - return err - } - - // load STS temp users - if err := store.loadUsers(ctx, stsUser, iamUsersMap); err != nil { - return err - } - - // load STS policy mappings - if err := store.loadMappedPolicies(ctx, stsUser, false, iamUserPolicyMap); err != nil { - return err - } - - for k, v := range iamPolicyDocsMap { - sys.iamPolicyDocsMap[k] = v - } - - // Merge the new reloaded entries into global map. - // See issue https://github.com/minio/minio/issues/9651 - // where the present list of entries on disk are not yet - // latest, there is a small window where this can make - // valid users invalid. - for k, v := range iamUsersMap { - sys.iamUsersMap[k] = v - } - - for k, v := range iamUserPolicyMap { - sys.iamUserPolicyMap[k] = v - } - - // purge any expired entries which became expired now. - for k, v := range sys.iamUsersMap { - if v.IsExpired() { - delete(sys.iamUsersMap, k) - delete(sys.iamUserPolicyMap, k) - // deleting will be done in the next cycle. - } - } - - for k, v := range iamGroupPolicyMap { - sys.iamGroupPolicyMap[k] = v - } - - for k, v := range iamGroupsMap { - sys.iamGroupsMap[k] = v - } - - sys.buildUserGroupMemberships() select { case <-sys.configLoaded: default: @@ -670,12 +296,11 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc // HasWatcher - returns if the IAM system has a watcher to be notified of // changes. func (sys *IAMSys) HasWatcher() bool { - _, ok := sys.store.(iamStorageWatcher) - return ok + return sys.store.HasWatcher() } func (sys *IAMSys) watch(ctx context.Context) { - watcher, ok := sys.store.(iamStorageWatcher) + watcher, ok := sys.store.IAMStorageAPI.(iamStorageWatcher) if ok { ch := watcher.watch(ctx, iamConfigPrefix) for event := range ch { @@ -715,99 +340,66 @@ func (sys *IAMSys) loadWatchedEvent(outerCtx context.Context, event iamWatchEven sys.Lock() defer sys.Unlock() - sys.store.lock() - defer sys.store.unlock() - if event.isCreated { switch { case usersPrefix: accessKey := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigUsersPrefix)) - err = sys.store.loadUser(ctx, accessKey, regUser, sys.iamUsersMap) + err = sys.store.UserNotificationHandler(ctx, accessKey, regUser) case stsPrefix: accessKey := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigSTSPrefix)) - err = sys.store.loadUser(ctx, accessKey, stsUser, sys.iamUsersMap) - if err == nil { - // We need to update the policy map for the - // parent below, so we retrieve the credentials - // just added. - creds, ok := sys.iamUsersMap[accessKey] - if !ok { - // This could happen, if the credential - // being loaded has expired. - break - } - - // We are on purpose not persisting the policy map for parent - // user, although this is a hack, it is a good enough hack - // at this point in time - we need to overhaul our OIDC - // usage with service accounts with a more cleaner implementation - // - // This mapping is necessary to ensure that valid credentials - // have necessary ParentUser present - this is mainly for only - // webIdentity based STS tokens. - parentAccessKey := creds.ParentUser - if parentAccessKey != "" && parentAccessKey != globalActiveCred.AccessKey { - if _, ok := sys.iamUserPolicyMap[parentAccessKey]; !ok { - sys.iamUserPolicyMap[parentAccessKey] = sys.iamUserPolicyMap[accessKey] - } - } - } + err = sys.store.UserNotificationHandler(ctx, accessKey, stsUser) case svcPrefix: accessKey := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigServiceAccountsPrefix)) - err = sys.store.loadUser(ctx, accessKey, svcUser, sys.iamUsersMap) + err = sys.store.UserNotificationHandler(ctx, accessKey, svcUser) case groupsPrefix: group := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigGroupsPrefix)) - err = sys.store.loadGroup(ctx, group, sys.iamGroupsMap) - if err == nil { - gi := sys.iamGroupsMap[group] - sys.removeGroupFromMembershipsMap(group) - sys.updateGroupMembershipsMap(group, &gi) - } + err = sys.store.GroupNotificationHandler(ctx, group) case policyPrefix: policyName := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigPoliciesPrefix)) - err = sys.store.loadPolicyDoc(ctx, policyName, sys.iamPolicyDocsMap) + err = sys.store.PolicyNotificationHandler(ctx, policyName) case policyDBUsersPrefix: policyMapFile := strings.TrimPrefix(event.keyPath, iamConfigPolicyDBUsersPrefix) user := strings.TrimSuffix(policyMapFile, ".json") - err = sys.store.loadMappedPolicy(ctx, user, regUser, false, sys.iamUserPolicyMap) + err = sys.store.PolicyMappingNotificationHandler(ctx, user, false, regUser) case policyDBSTSUsersPrefix: policyMapFile := strings.TrimPrefix(event.keyPath, iamConfigPolicyDBSTSUsersPrefix) user := strings.TrimSuffix(policyMapFile, ".json") - err = sys.store.loadMappedPolicy(ctx, user, stsUser, false, sys.iamUserPolicyMap) + err = sys.store.PolicyMappingNotificationHandler(ctx, user, false, stsUser) case policyDBGroupsPrefix: policyMapFile := strings.TrimPrefix(event.keyPath, iamConfigPolicyDBGroupsPrefix) user := strings.TrimSuffix(policyMapFile, ".json") - err = sys.store.loadMappedPolicy(ctx, user, regUser, true, sys.iamGroupPolicyMap) + err = sys.store.PolicyMappingNotificationHandler(ctx, user, true, regUser) } } else { // delete event switch { case usersPrefix: accessKey := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigUsersPrefix)) - delete(sys.iamUsersMap, accessKey) + err = sys.store.UserNotificationHandler(ctx, accessKey, regUser) case stsPrefix: accessKey := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigSTSPrefix)) - delete(sys.iamUsersMap, accessKey) + err = sys.store.UserNotificationHandler(ctx, accessKey, stsUser) + case svcPrefix: + accessKey := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigServiceAccountsPrefix)) + err = sys.store.UserNotificationHandler(ctx, accessKey, svcUser) case groupsPrefix: group := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigGroupsPrefix)) - sys.removeGroupFromMembershipsMap(group) - delete(sys.iamGroupsMap, group) - delete(sys.iamGroupPolicyMap, group) + err = sys.store.GroupNotificationHandler(ctx, group) case policyPrefix: policyName := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigPoliciesPrefix)) - delete(sys.iamPolicyDocsMap, policyName) + err = sys.store.PolicyNotificationHandler(ctx, policyName) case policyDBUsersPrefix: policyMapFile := strings.TrimPrefix(event.keyPath, iamConfigPolicyDBUsersPrefix) user := strings.TrimSuffix(policyMapFile, ".json") - delete(sys.iamUserPolicyMap, user) + err = sys.store.PolicyMappingNotificationHandler(ctx, user, false, regUser) case policyDBSTSUsersPrefix: policyMapFile := strings.TrimPrefix(event.keyPath, iamConfigPolicyDBSTSUsersPrefix) user := strings.TrimSuffix(policyMapFile, ".json") - delete(sys.iamUserPolicyMap, user) + err = sys.store.PolicyMappingNotificationHandler(ctx, user, false, stsUser) case policyDBGroupsPrefix: policyMapFile := strings.TrimPrefix(event.keyPath, iamConfigPolicyDBGroupsPrefix) user := strings.TrimSuffix(policyMapFile, ".json") - delete(sys.iamGroupPolicyMap, user) + err = sys.store.PolicyMappingNotificationHandler(ctx, user, true, regUser) } } return err @@ -819,52 +411,7 @@ func (sys *IAMSys) DeletePolicy(policyName string) error { return errServerNotInitialized } - if policyName == "" { - return errInvalidArgument - } - - sys.store.lock() - defer sys.store.unlock() - - err := sys.store.deletePolicyDoc(context.Background(), policyName) - if err == errNoSuchPolicy { - // Ignore error if policy is already deleted. - err = nil - } - - delete(sys.iamPolicyDocsMap, policyName) - - // Delete user-policy mappings that will no longer apply - for u, mp := range sys.iamUserPolicyMap { - pset := mp.policySet() - if pset.Contains(policyName) { - cr, ok := sys.iamUsersMap[u] - if !ok { - // This case can happen when an temporary account - // is deleted or expired, removed it from userPolicyMap. - delete(sys.iamUserPolicyMap, u) - continue - } - pset.Remove(policyName) - // User is from STS if the cred are temporary - if cr.IsTemp() { - sys.policyDBSet(u, strings.Join(pset.ToSlice(), ","), stsUser, false) - } else { - sys.policyDBSet(u, strings.Join(pset.ToSlice(), ","), regUser, false) - } - } - } - - // Delete group-policy mappings that will no longer apply - for g, mp := range sys.iamGroupPolicyMap { - pset := mp.policySet() - if pset.Contains(policyName) { - pset.Remove(policyName) - sys.policyDBSet(g, strings.Join(pset.ToSlice(), ","), regUser, true) - } - } - - return err + return sys.store.DeletePolicy(context.Background(), policyName) } // InfoPolicy - expands the canned policy into its JSON structure. @@ -873,21 +420,7 @@ func (sys *IAMSys) InfoPolicy(policyName string) (iampolicy.Policy, error) { return iampolicy.Policy{}, errServerNotInitialized } - sys.store.rlock() - defer sys.store.runlock() - - var combinedPolicy iampolicy.Policy - for _, policy := range strings.Split(policyName, ",") { - if policy == "" { - continue - } - v, ok := sys.iamPolicyDocsMap[policy] - if !ok { - return iampolicy.Policy{}, errNoSuchPolicy - } - combinedPolicy = combinedPolicy.Merge(v) - } - return combinedPolicy, nil + return sys.store.GetPolicy(policyName) } // ListPolicies - lists all canned policies. @@ -898,40 +431,16 @@ func (sys *IAMSys) ListPolicies(bucketName string) (map[string]iampolicy.Policy, <-sys.configLoaded - sys.store.rlock() - defer sys.store.runlock() - - policyDocsMap := make(map[string]iampolicy.Policy, len(sys.iamPolicyDocsMap)) - for k, v := range sys.iamPolicyDocsMap { - if bucketName != "" && v.MatchResource(bucketName) { - policyDocsMap[k] = v - } else { - policyDocsMap[k] = v - } - } - - return policyDocsMap, nil + return sys.store.ListPolicies(context.Background(), bucketName) } -// SetPolicy - sets a new name policy. +// SetPolicy - sets a new named policy. func (sys *IAMSys) SetPolicy(policyName string, p iampolicy.Policy) error { if !sys.Initialized() { return errServerNotInitialized } - if p.IsEmpty() || policyName == "" { - return errInvalidArgument - } - - sys.store.lock() - defer sys.store.unlock() - - if err := sys.store.savePolicyDoc(context.Background(), policyName, p); err != nil { - return err - } - - sys.iamPolicyDocsMap[policyName] = p - return nil + return sys.store.SetPolicy(context.Background(), policyName, p) } // DeleteUser - delete user (only for long-term users not STS users). @@ -940,56 +449,7 @@ func (sys *IAMSys) DeleteUser(accessKey string) error { return errServerNotInitialized } - if sys.usersSysType != MinIOUsersSysType { - return errIAMActionNotAllowed - } - - // First we remove the user from their groups. - userInfo, getErr := sys.GetUserInfo(accessKey) - if getErr != nil { - return getErr - } - - for _, group := range userInfo.MemberOf { - removeErr := sys.RemoveUsersFromGroup(group, []string{accessKey}) - if removeErr != nil { - return removeErr - } - } - - // Next we can remove the user from memory and IAM store - sys.store.lock() - defer sys.store.unlock() - - for _, u := range sys.iamUsersMap { - // Delete any service accounts if any first. - if u.IsServiceAccount() { - if u.ParentUser == accessKey { - _ = sys.store.deleteUserIdentity(context.Background(), u.AccessKey, svcUser) - delete(sys.iamUsersMap, u.AccessKey) - } - } - // Delete any associated STS users. - if u.IsTemp() { - if u.ParentUser == accessKey { - _ = sys.store.deleteUserIdentity(context.Background(), u.AccessKey, stsUser) - delete(sys.iamUsersMap, u.AccessKey) - } - } - } - - // It is ok to ignore deletion error on the mapped policy - sys.store.deleteMappedPolicy(context.Background(), accessKey, regUser, false) - err := sys.store.deleteUserIdentity(context.Background(), accessKey, regUser) - if err == errNoSuchUser { - // ignore if user is already deleted. - err = nil - } - - delete(sys.iamUsersMap, accessKey) - delete(sys.iamUserPolicyMap, accessKey) - - return err + return sys.store.DeleteUser(context.Background(), accessKey, regUser) } // CurrentPolicies - returns comma separated policy string, from @@ -1000,18 +460,8 @@ func (sys *IAMSys) CurrentPolicies(policyName string) string { return "" } - sys.store.rlock() - defer sys.store.runlock() - - var policies []string - mp := newMappedPolicy(policyName) - for _, policy := range mp.toSlice() { - _, found := sys.iamPolicyDocsMap[policy] - if found { - policies = append(policies, policy) - } - } - return strings.Join(policies, ",") + policies, _ := sys.store.FilterPolicies(policyName, "") + return policies } // SetTempUser - set temporary user credentials, these credentials have an expiry. @@ -1020,101 +470,23 @@ func (sys *IAMSys) SetTempUser(accessKey string, cred auth.Credentials, policyNa return errServerNotInitialized } - ttl := int64(cred.Expiration.Sub(UTCNow()).Seconds()) - - // If OPA is not set we honor any policy claims for this - // temporary user which match with pre-configured canned - // policies for this server. - if globalPolicyOPA == nil && policyName != "" { - mp := newMappedPolicy(policyName) - combinedPolicy := sys.GetCombinedPolicy(mp.toSlice()...) - - if combinedPolicy.IsEmpty() { - return fmt.Errorf("specified policy %s, not found %w", policyName, errNoSuchPolicy) - } - - sys.store.lock() - defer sys.store.unlock() - - if err := sys.store.saveMappedPolicy(context.Background(), accessKey, stsUser, false, mp, options{ttl: ttl}); err != nil { - return err - } - - sys.iamUserPolicyMap[accessKey] = mp - - // We are on purpose not persisting the policy map for parent - // user, although this is a hack, it is a good enough hack - // at this point in time - we need to overhaul our OIDC - // usage with service accounts with a more cleaner implementation - // - // This mapping is necessary to ensure that valid credentials - // have necessary ParentUser present - this is mainly for only - // webIdentity based STS tokens. - if cred.IsTemp() && cred.ParentUser != "" && cred.ParentUser != globalActiveCred.AccessKey { - if _, ok := sys.iamUserPolicyMap[cred.ParentUser]; !ok { - sys.iamUserPolicyMap[cred.ParentUser] = mp - } - } - } else { - sys.store.lock() - defer sys.store.unlock() + if globalPolicyOPA != nil { + // If OPA is set, we do not need to set a policy mapping. + policyName = "" } - u := newUserIdentity(cred) - if err := sys.store.saveUserIdentity(context.Background(), accessKey, stsUser, u, options{ttl: ttl}); err != nil { - return err - } - - sys.iamUsersMap[accessKey] = cred - return nil + return sys.store.SetTempUser(context.Background(), accessKey, cred, policyName) } // ListBucketUsers - list all users who can access this 'bucket' func (sys *IAMSys) ListBucketUsers(bucket string) (map[string]madmin.UserInfo, error) { - if bucket == "" { - return nil, errInvalidArgument + if !sys.Initialized() { + return nil, errServerNotInitialized } - sys.store.rlock() - defer sys.store.runlock() + <-sys.configLoaded - var users = make(map[string]madmin.UserInfo) - - for k, v := range sys.iamUsersMap { - if v.IsTemp() || v.IsServiceAccount() { - continue - } - var policies []string - mp, ok := sys.iamUserPolicyMap[k] - if ok { - policies = append(policies, mp.toSlice()...) - for _, group := range sys.iamUserGroupMemberships[k].ToSlice() { - if nmp, ok := sys.iamGroupPolicyMap[group]; ok { - policies = append(policies, nmp.toSlice()...) - } - } - } - var matchesPolices []string - for _, p := range policies { - if sys.iamPolicyDocsMap[p].MatchResource(bucket) { - matchesPolices = append(matchesPolices, p) - } - } - if len(matchesPolices) > 0 { - users[k] = madmin.UserInfo{ - PolicyName: strings.Join(matchesPolices, ","), - Status: func() madmin.AccountStatus { - if v.IsValid() { - return madmin.AccountEnabled - } - return madmin.AccountDisabled - }(), - MemberOf: sys.iamUserGroupMemberships[k].ToSlice(), - } - } - } - - return users, nil + return sys.store.GetBucketUsers(bucket) } // ListUsers - list all users. @@ -1125,36 +497,7 @@ func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) { <-sys.configLoaded - sys.store.rlock() - defer sys.store.runlock() - - var users = make(map[string]madmin.UserInfo) - - for k, v := range sys.iamUsersMap { - if !v.IsTemp() && !v.IsServiceAccount() { - users[k] = madmin.UserInfo{ - PolicyName: sys.iamUserPolicyMap[k].Policies, - Status: func() madmin.AccountStatus { - if v.IsValid() { - return madmin.AccountEnabled - } - return madmin.AccountDisabled - }(), - MemberOf: sys.iamUserGroupMemberships[k].ToSlice(), - } - } - } - - if sys.usersSysType == LDAPUsersSysType { - for k, v := range sys.iamUserPolicyMap { - users[k] = madmin.UserInfo{ - PolicyName: v.Policies, - Status: madmin.AccountEnabled, - } - } - } - - return users, nil + return sys.store.GetUsers(), nil } // IsTempUser - returns if given key is a temporary user. @@ -1163,10 +506,7 @@ func (sys *IAMSys) IsTempUser(name string) (bool, string, error) { return false, "", errServerNotInitialized } - sys.store.rlock() - defer sys.store.runlock() - - cred, found := sys.iamUsersMap[name] + cred, found := sys.store.GetUser(name) if !found { return false, "", errNoSuchUser } @@ -1184,10 +524,7 @@ func (sys *IAMSys) IsServiceAccount(name string) (bool, string, error) { return false, "", errServerNotInitialized } - sys.store.rlock() - defer sys.store.runlock() - - cred, found := sys.iamUsersMap[name] + cred, found := sys.store.GetUser(name) if !found { return false, "", errNoSuchUser } @@ -1208,54 +545,10 @@ func (sys *IAMSys) GetUserInfo(name string) (u madmin.UserInfo, err error) { select { case <-sys.configLoaded: default: - sys.loadUserFromStore(name) + sys.store.LoadUser(context.Background(), name) } - if sys.usersSysType != MinIOUsersSysType { - sys.store.rlock() - // If the user has a mapped policy or is a member of a group, we - // return that info. Otherwise we return error. - var groups []string - for _, v := range sys.iamUsersMap { - if v.ParentUser == name { - groups = v.Groups - break - } - } - mappedPolicy, ok := sys.iamUserPolicyMap[name] - sys.store.runlock() - if !ok { - return u, errNoSuchUser - } - return madmin.UserInfo{ - PolicyName: mappedPolicy.Policies, - MemberOf: groups, - }, nil - } - - sys.store.rlock() - defer sys.store.runlock() - - cred, found := sys.iamUsersMap[name] - if !found { - return u, errNoSuchUser - } - - if cred.IsTemp() || cred.IsServiceAccount() { - return u, errIAMActionNotAllowed - } - - return madmin.UserInfo{ - PolicyName: sys.iamUserPolicyMap[name].Policies, - Status: func() madmin.AccountStatus { - if cred.IsValid() { - return madmin.AccountEnabled - } - return madmin.AccountDisabled - }(), - MemberOf: sys.iamUserGroupMemberships[name].ToSlice(), - }, nil - + return sys.store.GetUserInfo(name) } // SetUserStatus - sets current user status, supports disabled or enabled. @@ -1268,39 +561,7 @@ func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus) return errIAMActionNotAllowed } - if status != madmin.AccountEnabled && status != madmin.AccountDisabled { - return errInvalidArgument - } - - sys.store.lock() - defer sys.store.unlock() - - cred, ok := sys.iamUsersMap[accessKey] - if !ok { - return errNoSuchUser - } - - if cred.IsTemp() || cred.IsServiceAccount() { - return errIAMActionNotAllowed - } - - uinfo := newUserIdentity(auth.Credentials{ - AccessKey: accessKey, - SecretKey: cred.SecretKey, - Status: func() string { - if status == madmin.AccountEnabled { - return auth.AccountOn - } - return auth.AccountOff - }(), - }) - - if err := sys.store.saveUserIdentity(context.Background(), accessKey, regUser, uinfo); err != nil { - return err - } - - sys.iamUsersMap[accessKey] = uinfo.Credentials - return nil + return sys.store.SetUserStatus(context.Background(), accessKey, status) } type newServiceAccountOpts struct { @@ -1342,50 +603,6 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro return auth.Credentials{}, errIAMActionNotAllowed } - sys.store.lock() - defer sys.store.unlock() - - // Handle validation of incoming service accounts. - { - cr, found := sys.iamUsersMap[opts.accessKey] - // found newly requested service account, to be an existing - // user, reject such operations. - if found && !cr.IsTemp() && !cr.IsServiceAccount() { - return auth.Credentials{}, errIAMActionNotAllowed - } - // found newly requested service account, to be an existing - // temporary user, reject such operations. - if found && cr.IsTemp() { - return auth.Credentials{}, errIAMActionNotAllowed - } - // found newly requested service account, to be an existing - // service account for another parentUser, reject such operations. - if found && cr.IsServiceAccount() && cr.ParentUser != parentUser { - return auth.Credentials{}, errIAMActionNotAllowed - } - } - - cr, found := sys.iamUsersMap[parentUser] - // Disallow service accounts to further create more service accounts. - if found && cr.IsServiceAccount() { - return auth.Credentials{}, errIAMActionNotAllowed - } - - policies, err := sys.policyDBGet(parentUser, false) - if err != nil { - return auth.Credentials{}, err - } - for _, group := range groups { - gpolicies, err := sys.policyDBGet(group, true) - if err != nil && err != errNoSuchGroup { - return auth.Credentials{}, err - } - policies = append(policies, gpolicies...) - } - if len(policies) == 0 { - return auth.Credentials{}, errNoSuchUser - } - m := make(map[string]interface{}) m[parentClaim] = parentUser @@ -1408,6 +625,7 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro cred auth.Credentials ) + var err error if len(opts.accessKey) > 0 { cred, err = auth.CreateNewCredentialsWithMetadata(opts.accessKey, opts.secretKey, m, globalActiveCred.SecretKey) } else { @@ -1420,14 +638,10 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro cred.Groups = groups cred.Status = string(auth.AccountOn) - u := newUserIdentity(cred) - - if err := sys.store.saveUserIdentity(context.Background(), u.Credentials.AccessKey, svcUser, u); err != nil { + err = sys.store.AddServiceAccount(ctx, cred) + if err != nil { return auth.Credentials{}, err } - - sys.iamUsersMap[u.Credentials.AccessKey] = u.Credentials - return cred, nil } @@ -1443,62 +657,7 @@ func (sys *IAMSys) UpdateServiceAccount(ctx context.Context, accessKey string, o return errServerNotInitialized } - sys.store.lock() - defer sys.store.unlock() - - cr, ok := sys.iamUsersMap[accessKey] - if !ok || !cr.IsServiceAccount() { - return errNoSuchServiceAccount - } - - if opts.secretKey != "" { - if !auth.IsSecretKeyValid(opts.secretKey) { - return auth.ErrInvalidSecretKeyLength - } - cr.SecretKey = opts.secretKey - } - - switch opts.status { - // The caller did not ask to update status account, do nothing - case "": - // Update account status - case auth.AccountOn, auth.AccountOff: - cr.Status = opts.status - default: - return errors.New("unknown account status value") - } - - if opts.sessionPolicy != nil { - m := make(map[string]interface{}) - err := opts.sessionPolicy.Validate() - if err != nil { - return err - } - policyBuf, err := json.Marshal(opts.sessionPolicy) - if err != nil { - return err - } - if len(policyBuf) > 16*humanize.KiByte { - return fmt.Errorf("Session policy should not exceed 16 KiB characters") - } - - m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf) - m[iamPolicyClaimNameSA()] = "embedded-policy" - m[parentClaim] = cr.ParentUser - cr.SessionToken, err = auth.JWTSignWithAccessKey(accessKey, m, globalActiveCred.SecretKey) - if err != nil { - return err - } - } - - u := newUserIdentity(cr) - if err := sys.store.saveUserIdentity(context.Background(), u.Credentials.AccessKey, svcUser, u); err != nil { - return err - } - - sys.iamUsersMap[u.Credentials.AccessKey] = u.Credentials - - return nil + return sys.store.UpdateServiceAccount(ctx, accessKey, opts) } // ListServiceAccounts - lists all services accounts associated to a specific user @@ -1509,20 +668,7 @@ func (sys *IAMSys) ListServiceAccounts(ctx context.Context, accessKey string) ([ <-sys.configLoaded - sys.store.rlock() - defer sys.store.runlock() - - var serviceAccounts []auth.Credentials - for _, v := range sys.iamUsersMap { - if v.IsServiceAccount() && v.ParentUser == accessKey { - // Hide secret key & session key here - v.SecretKey = "" - v.SessionToken = "" - serviceAccounts = append(serviceAccounts, v) - } - } - - return serviceAccounts, nil + return sys.store.ListServiceAccounts(ctx, accessKey) } // GetServiceAccount - gets information about a service account @@ -1531,10 +677,7 @@ func (sys *IAMSys) GetServiceAccount(ctx context.Context, accessKey string) (aut return auth.Credentials{}, nil, errServerNotInitialized } - sys.store.rlock() - defer sys.store.runlock() - - sa, ok := sys.iamUsersMap[accessKey] + sa, ok := sys.store.GetUser(accessKey) if !ok || !sa.IsServiceAccount() { return auth.Credentials{}, nil, errNoSuchServiceAccount } @@ -1574,10 +717,7 @@ func (sys *IAMSys) GetClaimsForSvcAcc(ctx context.Context, accessKey string) (ma return nil, nil } - sys.store.rlock() - defer sys.store.runlock() - - sa, ok := sys.iamUsersMap[accessKey] + sa, ok := sys.store.GetUser(accessKey) if !ok || !sa.IsServiceAccount() { return nil, errNoSuchServiceAccount } @@ -1595,22 +735,12 @@ func (sys *IAMSys) DeleteServiceAccount(ctx context.Context, accessKey string) e return errServerNotInitialized } - sys.store.lock() - defer sys.store.unlock() - - sa, ok := sys.iamUsersMap[accessKey] + sa, ok := sys.store.GetUser(accessKey) if !ok || !sa.IsServiceAccount() { return nil } - // It is ok to ignore deletion error on the mapped policy - err := sys.store.deleteUserIdentity(context.Background(), accessKey, svcUser) - if err != nil && err != errNoSuchUser { - return err - } - - delete(sys.iamUsersMap, accessKey) - return nil + return sys.store.DeleteUser(ctx, accessKey, svcUser) } // CreateUser - create new user credentials and policy, if user already exists @@ -1632,36 +762,7 @@ func (sys *IAMSys) CreateUser(accessKey string, uinfo madmin.UserInfo) error { return auth.ErrInvalidSecretKeyLength } - sys.store.lock() - defer sys.store.unlock() - - cr, ok := sys.iamUsersMap[accessKey] - if cr.IsTemp() && ok { - return errIAMActionNotAllowed - } - - u := newUserIdentity(auth.Credentials{ - AccessKey: accessKey, - SecretKey: uinfo.SecretKey, - Status: func() string { - if uinfo.Status == madmin.AccountEnabled { - return auth.AccountOn - } - return auth.AccountOff - }(), - }) - - if err := sys.store.saveUserIdentity(context.Background(), accessKey, regUser, u); err != nil { - return err - } - - sys.iamUsersMap[accessKey] = u.Credentials - - // Set policy if specified. - if uinfo.PolicyName != "" { - return sys.policyDBSet(accessKey, uinfo.PolicyName, regUser, false) - } - return nil + return sys.store.AddUser(context.Background(), accessKey, uinfo) } // SetUserSecretKey - sets user secret key @@ -1682,127 +783,47 @@ func (sys *IAMSys) SetUserSecretKey(accessKey string, secretKey string) error { return auth.ErrInvalidSecretKeyLength } - sys.store.lock() - defer sys.store.unlock() - - cred, ok := sys.iamUsersMap[accessKey] - if !ok { - return errNoSuchUser - } - - cred.SecretKey = secretKey - u := newUserIdentity(cred) - if err := sys.store.saveUserIdentity(context.Background(), accessKey, regUser, u); err != nil { - return err - } - - sys.iamUsersMap[accessKey] = cred - return nil -} - -func (sys *IAMSys) loadUserFromStore(accessKey string) { - sys.store.lock() - // If user is already found proceed. - if _, found := sys.iamUsersMap[accessKey]; !found { - sys.store.loadUser(context.Background(), accessKey, regUser, sys.iamUsersMap) - if _, found = sys.iamUsersMap[accessKey]; found { - // found user, load its mapped policies - sys.store.loadMappedPolicy(context.Background(), accessKey, regUser, false, sys.iamUserPolicyMap) - } else { - sys.store.loadUser(context.Background(), accessKey, svcUser, sys.iamUsersMap) - if svc, found := sys.iamUsersMap[accessKey]; found { - // Found service account, load its parent user and its mapped policies. - if sys.usersSysType == MinIOUsersSysType { - sys.store.loadUser(context.Background(), svc.ParentUser, regUser, sys.iamUsersMap) - } - sys.store.loadMappedPolicy(context.Background(), svc.ParentUser, regUser, false, sys.iamUserPolicyMap) - } else { - // None found fall back to STS users. - sys.store.loadUser(context.Background(), accessKey, stsUser, sys.iamUsersMap) - if _, found = sys.iamUsersMap[accessKey]; found { - // STS user found, load its mapped policy. - sys.store.loadMappedPolicy(context.Background(), accessKey, stsUser, false, sys.iamUserPolicyMap) - } - } - } - } - - // Load associated policies if any. - for _, policy := range sys.iamUserPolicyMap[accessKey].toSlice() { - if _, found := sys.iamPolicyDocsMap[policy]; !found { - sys.store.loadPolicyDoc(context.Background(), policy, sys.iamPolicyDocsMap) - } - } - - sys.buildUserGroupMemberships() - sys.store.unlock() + return sys.store.UpdateUserSecretKey(context.Background(), accessKey, secretKey) } // purgeExpiredCredentialsForExternalSSO - validates if local credentials are still valid // by checking remote IDP if the relevant users are still active and present. func (sys *IAMSys) purgeExpiredCredentialsForExternalSSO(ctx context.Context) { - sys.store.lock() - parentUsersMap := make(map[string][]auth.Credentials, len(sys.iamUsersMap)) - for _, cred := range sys.iamUsersMap { - if cred.IsServiceAccount() || cred.IsTemp() { - userid, err := parseOpenIDParentUser(cred.ParentUser) - if err == errSkipFile { - continue - } - parentUsersMap[userid] = append(parentUsersMap[userid], cred) + parentUsers := sys.store.GetAllParentUsers() + var expiredUsers []string + for _, parentUser := range parentUsers { + userid, err := parseOpenIDParentUser(parentUser) + if err == errSkipFile { + continue } - } - sys.store.unlock() - - expiredUsers := make([]auth.Credentials, 0, len(parentUsersMap)) - for userid, creds := range parentUsersMap { u, err := globalOpenIDConfig.LookupUser(userid) if err != nil { logger.LogIf(GlobalContext, err) continue } - // Disabled parentUser purge the entries locally + // If user is set to "disabled", we will remove them + // subsequently. if !u.Enabled { - expiredUsers = append(expiredUsers, creds...) + expiredUsers = append(expiredUsers, parentUser) } } - for _, cred := range expiredUsers { - userType := regUser - if cred.IsServiceAccount() { - userType = svcUser - } else if cred.IsTemp() { - userType = stsUser - } - sys.store.deleteIAMConfig(ctx, getUserIdentityPath(cred.AccessKey, userType)) - sys.store.deleteIAMConfig(ctx, getMappedPolicyPath(cred.AccessKey, userType, false)) - } - - sys.store.lock() - for _, cred := range expiredUsers { - delete(sys.iamUsersMap, cred.AccessKey) - delete(sys.iamUserPolicyMap, cred.AccessKey) - } - sys.store.unlock() + // We ignore any errors + _ = sys.store.DeleteUsers(ctx, expiredUsers) } // purgeExpiredCredentialsForLDAP - validates if local credentials are still // valid by checking LDAP server if the relevant users are still present. func (sys *IAMSys) purgeExpiredCredentialsForLDAP(ctx context.Context) { - sys.store.lock() - parentUsersMap := make(map[string][]auth.Credentials, len(sys.iamUsersMap)) - parentUsers := make([]string, 0, len(sys.iamUsersMap)) - for _, cred := range sys.iamUsersMap { - if cred.IsServiceAccount() || cred.IsTemp() { - if globalLDAPConfig.IsLDAPUserDN(cred.ParentUser) { - if _, ok := parentUsersMap[cred.ParentUser]; !ok { - parentUsers = append(parentUsers, cred.ParentUser) - } - parentUsersMap[cred.ParentUser] = append(parentUsersMap[cred.ParentUser], cred) - } + parentUsers := sys.store.GetAllParentUsers() + var allDistNames []string + for _, parentUser := range parentUsers { + if !globalLDAPConfig.IsLDAPUserDN(parentUser) { + continue } + + allDistNames = append(allDistNames, parentUser) } - sys.store.unlock() expiredUsers, err := globalLDAPConfig.GetNonEligibleUserDistNames(parentUsers) if err != nil { @@ -1811,71 +832,51 @@ func (sys *IAMSys) purgeExpiredCredentialsForLDAP(ctx context.Context) { return } - for _, expiredUser := range expiredUsers { - for _, cred := range parentUsersMap[expiredUser] { - userType := regUser - if cred.IsServiceAccount() { - userType = svcUser - } else if cred.IsTemp() { - userType = stsUser - } - sys.store.deleteIAMConfig(ctx, getUserIdentityPath(cred.AccessKey, userType)) - sys.store.deleteIAMConfig(ctx, getMappedPolicyPath(cred.AccessKey, userType, false)) - } - } - - sys.store.lock() - for _, user := range expiredUsers { - for _, cred := range parentUsersMap[user] { - delete(sys.iamUsersMap, cred.AccessKey) - delete(sys.iamUserPolicyMap, cred.AccessKey) - } - } - sys.store.unlock() + // We ignore any errors + _ = sys.store.DeleteUsers(ctx, expiredUsers) } // updateGroupMembershipsForLDAP - updates the list of groups associated with the credential. func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) { // 1. Collect all LDAP users with active creds. - sys.store.lock() + allCreds := sys.store.GetSTSAndServiceAccounts() // List of unique LDAP (parent) user DNs that have active creds - parentUsers := make([]string, 0, len(sys.iamUsersMap)) + var parentUsers []string // Map of LDAP user to list of active credential objects - parentUserToCredsMap := make(map[string][]auth.Credentials, len(sys.iamUsersMap)) + parentUserToCredsMap := make(map[string][]auth.Credentials) // DN to ldap username mapping for each LDAP user - parentUserToLDAPUsernameMap := make(map[string]string, len(sys.iamUsersMap)) - for _, cred := range sys.iamUsersMap { - if cred.IsServiceAccount() || cred.IsTemp() { - if globalLDAPConfig.IsLDAPUserDN(cred.ParentUser) { - // Check if this is the first time we are - // encountering this LDAP user. - if _, ok := parentUserToCredsMap[cred.ParentUser]; !ok { - // Try to find the ldapUsername for this - // parentUser by extracting JWT claims - jwtClaims, err := auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey) - if err != nil { - // skip this cred - session token seems - // invalid - continue - } - ldapUsername, ok := jwtClaims.Lookup(ldapUserN) - if !ok { - // skip this cred - we dont have the - // username info needed - continue - } - - // Collect each new cred.ParentUser into parentUsers - parentUsers = append(parentUsers, cred.ParentUser) - - // Update the ldapUsernameMap - parentUserToLDAPUsernameMap[cred.ParentUser] = ldapUsername - } - parentUserToCredsMap[cred.ParentUser] = append(parentUserToCredsMap[cred.ParentUser], cred) - } + parentUserToLDAPUsernameMap := make(map[string]string) + for _, cred := range allCreds { + if !globalLDAPConfig.IsLDAPUserDN(cred.ParentUser) { + continue } + // Check if this is the first time we are + // encountering this LDAP user. + if _, ok := parentUserToCredsMap[cred.ParentUser]; !ok { + // Try to find the ldapUsername for this + // parentUser by extracting JWT claims + jwtClaims, err := auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey) + if err != nil { + // skip this cred - session token seems + // invalid + continue + } + ldapUsername, ok := jwtClaims.Lookup(ldapUserN) + if !ok { + // skip this cred - we dont have the + // username info needed + continue + } + + // Collect each new cred.ParentUser into parentUsers + parentUsers = append(parentUsers, cred.ParentUser) + + // Update the ldapUsernameMap + parentUserToLDAPUsernameMap[cred.ParentUser] = ldapUsername + } + parentUserToCredsMap[cred.ParentUser] = append(parentUserToCredsMap[cred.ParentUser], cred) + } - sys.store.unlock() // 2. Query LDAP server for groups of the LDAP users collected. updatedGroups, err := globalLDAPConfig.LookupGroupMemberships(parentUsers, parentUserToLDAPUsernameMap) @@ -1886,8 +887,6 @@ func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) { } // 3. Update creds for those users whose groups are changed - sys.store.lock() - defer sys.store.unlock() for _, parentUser := range parentUsers { currGroupsSet := updatedGroups[parentUser] currGroups := currGroupsSet.ToSlice() @@ -1900,22 +899,10 @@ func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) { } cred.Groups = currGroups - userType := regUser - if cred.IsServiceAccount() { - userType = svcUser - } else if cred.IsTemp() { - userType = stsUser - } - // Overwrite the user identity here. As store should be - // atomic, it shouldn't cause any corruption. - if err := sys.store.saveUserIdentity(ctx, cred.AccessKey, userType, newUserIdentity(cred)); err != nil { + if err := sys.store.UpdateUserIdentity(ctx, cred); err != nil { // Log and continue error - perhaps it'll work the next time. logger.LogIf(GlobalContext, err) - continue } - // If we wrote the updated creds to IAM storage, we can - // update the in memory map. - sys.iamUsersMap[cred.AccessKey] = cred } } } @@ -1930,37 +917,32 @@ func (sys *IAMSys) GetUser(accessKey string) (cred auth.Credentials, ok bool) { select { case <-sys.configLoaded: default: - sys.loadUserFromStore(accessKey) + sys.store.LoadUser(context.Background(), accessKey) fallback = true } - sys.store.rlock() - cred, ok = sys.iamUsersMap[accessKey] + cred, ok = sys.store.GetUser(accessKey) if !ok && !fallback { - sys.store.runlock() // accessKey not found, also // IAM store is not in fallback mode // we can try to reload again from // the IAM store and see if credential // exists now. If it doesn't proceed to // fail. - sys.loadUserFromStore(accessKey) - - sys.store.rlock() - cred, ok = sys.iamUsersMap[accessKey] + sys.store.LoadUser(context.Background(), accessKey) + cred, ok = sys.store.GetUser(accessKey) } - defer sys.store.runlock() if ok && cred.IsValid() { if cred.IsServiceAccount() || cred.IsTemp() { - policies, err := sys.policyDBGet(cred.AccessKey, false) + policies, err := sys.store.PolicyDBGet(cred.AccessKey, false) if err != nil { // Reject if the policy map for user doesn't exist anymore. logger.LogIf(context.Background(), fmt.Errorf("'%s' user does not have a policy present", cred.ParentUser)) return auth.Credentials{}, false } for _, group := range cred.Groups { - ps, err := sys.policyDBGet(group, true) + ps, err := sys.store.PolicyDBGet(group, true) if err != nil { // Reject if the policy map for group doesn't exist anymore. logger.LogIf(context.Background(), fmt.Errorf("'%s' group does not have a policy present", group)) @@ -1981,57 +963,11 @@ func (sys *IAMSys) AddUsersToGroup(group string, members []string) error { return errServerNotInitialized } - if group == "" { - return errInvalidArgument - } - if sys.usersSysType != MinIOUsersSysType { return errIAMActionNotAllowed } - sys.store.lock() - defer sys.store.unlock() - - // Validate that all members exist. - for _, member := range members { - cr, ok := sys.iamUsersMap[member] - if !ok { - return errNoSuchUser - } - if cr.IsTemp() { - return errIAMActionNotAllowed - } - } - - gi, ok := sys.iamGroupsMap[group] - if !ok { - // Set group as enabled by default when it doesn't - // exist. - gi = newGroupInfo(members) - } else { - mergedMembers := append(gi.Members, members...) - uniqMembers := set.CreateStringSet(mergedMembers...).ToSlice() - gi.Members = uniqMembers - } - - if err := sys.store.saveGroupInfo(context.Background(), group, gi); err != nil { - return err - } - - sys.iamGroupsMap[group] = gi - - // update user-group membership map - for _, member := range members { - gset := sys.iamUserGroupMemberships[member] - if gset == nil { - gset = set.CreateStringSet(group) - } else { - gset.Add(group) - } - sys.iamUserGroupMemberships[member] = gset - } - - return nil + return sys.store.AddUsersToGroup(context.Background(), group, members) } // RemoveUsersFromGroup - remove users from group. If no users are @@ -2045,74 +981,7 @@ func (sys *IAMSys) RemoveUsersFromGroup(group string, members []string) error { return errIAMActionNotAllowed } - if group == "" { - return errInvalidArgument - } - - sys.store.lock() - defer sys.store.unlock() - - // Validate that all members exist. - for _, member := range members { - cr, ok := sys.iamUsersMap[member] - if !ok { - return errNoSuchUser - } - if cr.IsTemp() { - return errIAMActionNotAllowed - } - } - - gi, ok := sys.iamGroupsMap[group] - if !ok { - return errNoSuchGroup - } - - // Check if attempting to delete a non-empty group. - if len(members) == 0 && len(gi.Members) != 0 { - return errGroupNotEmpty - } - - if len(members) == 0 { - // len(gi.Members) == 0 here. - - // Remove the group from storage. First delete the - // mapped policy. No-mapped-policy case is ignored. - if err := sys.store.deleteMappedPolicy(context.Background(), group, regUser, true); err != nil && err != errNoSuchPolicy { - return err - } - if err := sys.store.deleteGroupInfo(context.Background(), group); err != nil && err != errNoSuchGroup { - return err - } - - // Delete from server memory - delete(sys.iamGroupsMap, group) - delete(sys.iamGroupPolicyMap, group) - return nil - } - - // Only removing members. - s := set.CreateStringSet(gi.Members...) - d := set.CreateStringSet(members...) - gi.Members = s.Difference(d).ToSlice() - - err := sys.store.saveGroupInfo(context.Background(), group, gi) - if err != nil { - return err - } - sys.iamGroupsMap[group] = gi - - // update user-group membership map - for _, member := range members { - gset := sys.iamUserGroupMemberships[member] - if gset == nil { - continue - } - gset.Remove(group) - sys.iamUserGroupMemberships[member] = gset - } - - return nil + return sys.store.RemoveUsersFromGroup(context.Background(), group, members) } // SetGroupStatus - enable/disabled a group @@ -2125,29 +994,7 @@ func (sys *IAMSys) SetGroupStatus(group string, enabled bool) error { return errIAMActionNotAllowed } - sys.store.lock() - defer sys.store.unlock() - - if group == "" { - return errInvalidArgument - } - - gi, ok := sys.iamGroupsMap[group] - if !ok { - return errNoSuchGroup - } - - if enabled { - gi.Status = statusEnabled - } else { - gi.Status = statusDisabled - } - - if err := sys.store.saveGroupInfo(context.Background(), group, gi); err != nil { - return err - } - sys.iamGroupsMap[group] = gi - return nil + return sys.store.SetGroupStatus(context.Background(), group, enabled) } // GetGroupDescription - builds up group description @@ -2156,34 +1003,7 @@ func (sys *IAMSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err e return gd, errServerNotInitialized } - ps, err := sys.PolicyDBGet(group, true) - if err != nil { - return gd, err - } - - policy := strings.Join(ps, ",") - - if sys.usersSysType != MinIOUsersSysType { - return madmin.GroupDesc{ - Name: group, - Policy: policy, - }, nil - } - - sys.store.rlock() - defer sys.store.runlock() - - gi, ok := sys.iamGroupsMap[group] - if !ok { - return gd, errNoSuchGroup - } - - return madmin.GroupDesc{ - Name: group, - Status: gi.Status, - Members: gi.Members, - Policy: policy, - }, nil + return sys.store.GetGroupDescription(group) } // ListGroups - lists groups. @@ -2194,21 +1014,7 @@ func (sys *IAMSys) ListGroups() (r []string, err error) { <-sys.configLoaded - sys.store.rlock() - defer sys.store.runlock() - - r = make([]string, 0, len(sys.iamGroupsMap)) - for k := range sys.iamGroupsMap { - r = append(r, k) - } - - if sys.usersSysType == LDAPUsersSysType { - for k := range sys.iamGroupPolicyMap { - r = append(r, k) - } - } - - return r, nil + return sys.store.ListGroups(context.Background()) } // PolicyDBSet - sets a policy for a user or group in the PolicyDB. @@ -2217,73 +1023,13 @@ func (sys *IAMSys) PolicyDBSet(name, policy string, isGroup bool) error { return errServerNotInitialized } - sys.store.lock() - defer sys.store.unlock() - + // Determine user-type based on IDP mode. + userType := regUser if sys.usersSysType == LDAPUsersSysType { - return sys.policyDBSet(name, policy, stsUser, isGroup) + userType = stsUser } - return sys.policyDBSet(name, policy, regUser, isGroup) -} - -// policyDBSet - sets a policy for user in the policy db. Assumes that caller -// has sys.Lock(). If policy == "", then policy mapping is removed. -func (sys *IAMSys) policyDBSet(name, policyName string, userType IAMUserType, isGroup bool) error { - if name == "" { - return errInvalidArgument - } - - if sys.usersSysType == MinIOUsersSysType { - if !isGroup { - if _, ok := sys.iamUsersMap[name]; !ok { - return errNoSuchUser - } - } else { - if _, ok := sys.iamGroupsMap[name]; !ok { - return errNoSuchGroup - } - } - } - - // Handle policy mapping removal - if policyName == "" { - if sys.usersSysType == LDAPUsersSysType { - // Add a fallback removal towards previous content that may come back - // as a ghost user due to lack of delete, this change occurred - // introduced in PR #11840 - sys.store.deleteMappedPolicy(context.Background(), name, regUser, false) - } - err := sys.store.deleteMappedPolicy(context.Background(), name, userType, isGroup) - if err != nil && err != errNoSuchPolicy { - return err - } - if !isGroup { - delete(sys.iamUserPolicyMap, name) - } else { - delete(sys.iamGroupPolicyMap, name) - } - return nil - } - - mp := newMappedPolicy(policyName) - for _, policy := range mp.toSlice() { - if _, found := sys.iamPolicyDocsMap[policy]; !found { - logger.LogIf(GlobalContext, fmt.Errorf("%w: (%s)", errNoSuchPolicy, policy)) - return errNoSuchPolicy - } - } - - // Handle policy mapping set/update - if err := sys.store.saveMappedPolicy(context.Background(), name, userType, isGroup, mp); err != nil { - return err - } - if !isGroup { - sys.iamUserPolicyMap[name] = mp - } else { - sys.iamGroupPolicyMap[name] = mp - } - return nil + return sys.store.PolicyDBSet(context.Background(), name, policy, userType, isGroup) } // PolicyDBGet - gets policy set on a user or group. If a list of groups is @@ -2293,102 +1039,7 @@ func (sys *IAMSys) PolicyDBGet(name string, isGroup bool, groups ...string) ([]s return nil, errServerNotInitialized } - if name == "" { - return nil, errInvalidArgument - } - - sys.store.rlock() - defer sys.store.runlock() - - policies, err := sys.policyDBGet(name, isGroup) - if err != nil { - return nil, err - } - - if !isGroup { - for _, group := range groups { - ps, err := sys.policyDBGet(group, true) - if err != nil { - return nil, err - } - policies = append(policies, ps...) - } - } - - return policies, nil -} - -// This call assumes that caller has the sys.RLock(). -// -// If a group is passed, it returns policies associated with the group. -// -// If a user is passed, it returns policies of the user along with any groups -// that the server knows the user is a member of. -// -// In LDAP users mode, the server does not store any group membership -// information in IAM (i.e sys.iam*Map) - this info is stored only in the STS -// generated credentials. Thus we skip looking up group memberships, user map, -// and group map and check the appropriate policy maps directly. -func (sys *IAMSys) policyDBGet(name string, isGroup bool) (policies []string, err error) { - if isGroup { - if sys.usersSysType == MinIOUsersSysType { - g, ok := sys.iamGroupsMap[name] - if !ok { - return nil, errNoSuchGroup - } - - // Group is disabled, so we return no policy - this - // ensures the request is denied. - if g.Status == statusDisabled { - return nil, nil - } - } - - return sys.iamGroupPolicyMap[name].toSlice(), nil - } - - if name == globalActiveCred.AccessKey { - return []string{"consoleAdmin"}, nil - } - - // When looking for a user's policies, we also check if the user - // and the groups they are member of are enabled. - var parentName string - u, ok := sys.iamUsersMap[name] - if ok { - if !u.IsValid() { - return nil, nil - } - parentName = u.ParentUser - } - - mp, ok := sys.iamUserPolicyMap[name] - if !ok { - // Service accounts with root credentials, inherit parent permissions - if parentName == globalActiveCred.AccessKey && u.IsServiceAccount() { - // even if this is set, the claims present in the service - // accounts apply the final permissions if any. - return []string{"consoleAdmin"}, nil - } - if parentName != "" { - mp = sys.iamUserPolicyMap[parentName] - } - } - - // returned policy could be empty - policies = append(policies, mp.toSlice()...) - - for _, group := range sys.iamUserGroupMemberships[name].ToSlice() { - // Skip missing or disabled groups - gi, ok := sys.iamGroupsMap[group] - if !ok || gi.Status == statusDisabled { - continue - } - - policies = append(policies, sys.iamGroupPolicyMap[group].toSlice()...) - } - - return policies, nil + return sys.store.PolicyDBGet(name, isGroup, groups...) } // IsAllowedServiceAccount - checks if the given service account is allowed to perform @@ -2425,28 +1076,12 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin return false } - var availablePolicies []iampolicy.Policy - // Policies were found, evaluate all of them. - sys.store.rlock() - for _, pname := range svcPolicies { - p, found := sys.iamPolicyDocsMap[pname] - if found { - availablePolicies = append(availablePolicies, p) - } - } - sys.store.runlock() - - if len(availablePolicies) == 0 { + availablePoliciesStr, combinedPolicy := sys.store.FilterPolicies(strings.Join(svcPolicies, ","), "") + if availablePoliciesStr == "" { return false } - combinedPolicy := availablePolicies[0] - for i := 1; i < len(availablePolicies); i++ { - combinedPolicy.Statements = append(combinedPolicy.Statements, - availablePolicies[i].Statements...) - } - parentArgs := args parentArgs.AccountName = parentUser // These are dynamic values set them appropriately. @@ -2526,29 +1161,12 @@ func (sys *IAMSys) IsAllowedLDAPSTS(args iampolicy.Args, parentUser string) bool return false } - var availablePolicies []iampolicy.Policy - // Policies were found, evaluate all of them. - sys.store.rlock() - for _, pname := range ldapPolicies { - p, found := sys.iamPolicyDocsMap[pname] - if found { - availablePolicies = append(availablePolicies, p) - } - } - sys.store.runlock() - - if len(availablePolicies) == 0 { + availablePoliciesStr, combinedPolicy := sys.store.FilterPolicies(strings.Join(ldapPolicies, ","), "") + if availablePoliciesStr == "" { return false } - combinedPolicy := availablePolicies[0] - for i := 1; i < len(availablePolicies); i++ { - combinedPolicy.Statements = - append(combinedPolicy.Statements, - availablePolicies[i].Statements...) - } - hasSessionPolicy, isAllowedSP := isAllowedBySessionPolicy(args) if hasSessionPolicy { return isAllowedSP && combinedPolicy.IsAllowed(args) @@ -2579,11 +1197,8 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool { return false } - sys.store.rlock() - defer sys.store.runlock() - // If policy is available for given user, check the policy. - mp, ok := sys.iamUserPolicyMap[args.AccountName] + mp, ok := sys.store.GetMappedPolicy(args.AccountName, false) if !ok { // No policy set for the user that we can find, no access! return false @@ -2596,21 +1211,18 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool { return false } - var availablePolicies []iampolicy.Policy - for pname := range policies { - p, found := sys.iamPolicyDocsMap[pname] - if !found { - // all policies presented in the claim should exist - logger.LogIf(GlobalContext, fmt.Errorf("expected policy (%s) missing from the JWT claim %s, rejecting the request", pname, iamPolicyClaimNameOpenID())) - return false + combinedPolicy, err := sys.store.GetPolicy(strings.Join(policies.ToSlice(), ",")) + if err == errNoSuchPolicy { + for pname := range policies { + _, err := sys.store.GetPolicy(pname) + if err == errNoSuchPolicy { + // all policies presented in the claim should exist + logger.LogIf(GlobalContext, fmt.Errorf("expected policy (%s) missing from the JWT claim %s, rejecting the request", pname, iamPolicyClaimNameOpenID())) + return false + } } - availablePolicies = append(availablePolicies, p) - } - - combinedPolicy := availablePolicies[0] - for i := 1; i < len(availablePolicies); i++ { - combinedPolicy.Statements = append(combinedPolicy.Statements, - availablePolicies[i].Statements...) + logger.LogIf(GlobalContext, fmt.Errorf("all policies were unexpectedly present!")) + return false } // These are dynamic values set them appropriately. @@ -2666,29 +1278,8 @@ func isAllowedBySessionPolicy(args iampolicy.Args) (hasSessionPolicy bool, isAll // GetCombinedPolicy returns a combined policy combining all policies func (sys *IAMSys) GetCombinedPolicy(policies ...string) iampolicy.Policy { - // Policies were found, evaluate all of them. - sys.store.rlock() - defer sys.store.runlock() - - var availablePolicies []iampolicy.Policy - for _, pname := range policies { - p, found := sys.iamPolicyDocsMap[pname] - if found { - availablePolicies = append(availablePolicies, p) - } - } - - if len(availablePolicies) == 0 { - return iampolicy.Policy{} - } - - combinedPolicy := availablePolicies[0] - for i := 1; i < len(availablePolicies); i++ { - combinedPolicy.Statements = append(combinedPolicy.Statements, - availablePolicies[i].Statements...) - } - - return combinedPolicy + _, policy := sys.store.FilterPolicies(strings.Join(policies, ","), "") + return policy } // IsAllowed - checks given policy args is allowed to continue the Rest API. @@ -2764,43 +1355,6 @@ func setDefaultCannedPolicies(policies map[string]iampolicy.Policy) { } } -// buildUserGroupMemberships - builds the memberships map. IMPORTANT: -// Assumes that sys.Lock is held by caller. -func (sys *IAMSys) buildUserGroupMemberships() { - for group, gi := range sys.iamGroupsMap { - sys.updateGroupMembershipsMap(group, &gi) - } -} - -// updateGroupMembershipsMap - updates the memberships map for a -// group. IMPORTANT: Assumes sys.Lock() is held by caller. -func (sys *IAMSys) updateGroupMembershipsMap(group string, gi *GroupInfo) { - if gi == nil { - return - } - for _, member := range gi.Members { - v := sys.iamUserGroupMemberships[member] - if v == nil { - v = set.CreateStringSet(group) - } else { - v.Add(group) - } - sys.iamUserGroupMemberships[member] = v - } -} - -// removeGroupFromMembershipsMap - removes the group from every member -// in the cache. IMPORTANT: Assumes sys.Lock() is held by caller. -func (sys *IAMSys) removeGroupFromMembershipsMap(group string) { - for member, groups := range sys.iamUserGroupMemberships { - if !groups.Contains(group) { - continue - } - groups.Remove(group) - sys.iamUserGroupMemberships[member] = groups - } -} - // EnableLDAPSys - enable ldap system users type. func (sys *IAMSys) EnableLDAPSys() { sys.usersSysType = LDAPUsersSysType @@ -2809,13 +1363,7 @@ func (sys *IAMSys) EnableLDAPSys() { // NewIAMSys - creates new config system object. func NewIAMSys() *IAMSys { return &IAMSys{ - usersSysType: MinIOUsersSysType, - iamUsersMap: make(map[string]auth.Credentials), - iamPolicyDocsMap: make(map[string]iampolicy.Policy), - iamUserPolicyMap: make(map[string]MappedPolicy), - iamGroupPolicyMap: make(map[string]MappedPolicy), - iamGroupsMap: make(map[string]GroupInfo), - iamUserGroupMemberships: make(map[string]set.StringSet), - configLoaded: make(chan struct{}), + usersSysType: MinIOUsersSysType, + configLoaded: make(chan struct{}), } } diff --git a/cmd/site-replication.go b/cmd/site-replication.go index a2efe2364..f24ea8e9d 100644 --- a/cmd/site-replication.go +++ b/cmd/site-replication.go @@ -1270,9 +1270,7 @@ func (c *SiteReplicationSys) getAdminClient(ctx context.Context, deploymentID st } func (c *SiteReplicationSys) getPeerCreds() (*auth.Credentials, error) { - globalIAMSys.store.rlock() - defer globalIAMSys.store.runlock() - creds, ok := globalIAMSys.iamUsersMap[c.state.ServiceAccountAccessKey] + creds, ok := globalIAMSys.store.GetUser(c.state.ServiceAccountAccessKey) if !ok { return nil, errors.New("site replication service account not found!") } diff --git a/cmd/sts-handlers_test.go b/cmd/sts-handlers_test.go index 4f36c2407..90c75dfcb 100644 --- a/cmd/sts-handlers_test.go +++ b/cmd/sts-handlers_test.go @@ -95,6 +95,10 @@ func (s *TestSuiteIAM) TestSTS(c *check) { 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, diff --git a/cmd/typed-errors.go b/cmd/typed-errors.go index 0aa6adba1..8565431ea 100644 --- a/cmd/typed-errors.go +++ b/cmd/typed-errors.go @@ -81,6 +81,9 @@ var errGroupNotEmpty = errors.New("Specified group is not empty - cannot remove // error returned in IAM subsystem when policy doesn't exist. var errNoSuchPolicy = errors.New("Specified canned policy does not exist") +// error returned when policy to be deleted is in use. +var errPolicyInUse = errors.New("Specified policy is in use and cannot be deleted.") + // error returned in IAM subsystem when an external users systems is configured. var errIAMActionNotAllowed = errors.New("Specified IAM action is not allowed")