From ecd54b4cba1bbda0744b0013324a746882bde246 Mon Sep 17 00:00:00 2001 From: Aditya Manthramurthy Date: Wed, 3 Nov 2021 19:47:49 -0700 Subject: [PATCH] Move all IAM storage functionality into iam store type (#13567) This reverts commit 091a7ae3590a510c51cd84981dccd6d45879b1d6. - Ensure all actions accessing storage lock properly. - Behavior change: policies can be deleted only when they are not associated with any active credentials. Also adds fix for accidental canned policy removal that was present in the reverted version of the change. --- cmd/admin-handler-utils.go | 6 + cmd/admin-handlers-users_test.go | 17 +- cmd/iam-dummy-store.go | 29 +- cmd/iam-etcd-store.go | 38 +- cmd/iam-object-store.go | 36 +- cmd/iam-store.go | 1716 ++++++++++++++++++++++++++++ cmd/iam.go | 1806 +++--------------------------- cmd/site-replication.go | 23 +- cmd/sts-handlers_test.go | 4 + cmd/typed-errors.go | 3 + go.mod | 2 +- go.sum | 4 + 12 files changed, 1992 insertions(+), 1692 deletions(-) create mode 100644 cmd/iam-store.go 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..4846b995d --- /dev/null +++ b/cmd/iam-store.go @@ -0,0 +1,1716 @@ +// 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 { + return set.CreateStringSet(mp.toSlice()...) +} + +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 +} + +// Set default canned policies only if not already overridden by users. +func setDefaultCannedPolicies(policies map[string]iampolicy.Policy) { + for _, v := range iampolicy.DefaultPolicies { + if _, ok := policies[v.Name]; !ok { + policies[v.Name] = v.Definition + } + } +} + +// 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() + + 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.lock() + defer store.unlock() + + m := map[string]iampolicy.Policy{} + err := store.loadPolicyDocs(ctx, m) + if err != nil { + return nil, err + } + + // Sets default canned policies + setDefaultCannedPolicies(m) + + 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..5aaf82c19 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. @@ -2740,67 +1331,6 @@ func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool { return sys.GetCombinedPolicy(policies...).IsAllowed(args) } -// Set default canned policies only if not already overridden by users. -func setDefaultCannedPolicies(policies map[string]iampolicy.Policy) { - _, ok := policies["writeonly"] - if !ok { - policies["writeonly"] = iampolicy.WriteOnly - } - _, ok = policies["readonly"] - if !ok { - policies["readonly"] = iampolicy.ReadOnly - } - _, ok = policies["readwrite"] - if !ok { - policies["readwrite"] = iampolicy.ReadWrite - } - _, ok = policies["diagnostics"] - if !ok { - policies["diagnostics"] = iampolicy.AdminDiagnostics - } - _, ok = policies["consoleAdmin"] - if !ok { - policies["consoleAdmin"] = iampolicy.Admin - } -} - -// 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 +1339,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..68ac3ba0a 100644 --- a/cmd/site-replication.go +++ b/cmd/site-replication.go @@ -20,7 +20,6 @@ package cmd import ( "bytes" "context" - "crypto/rand" "crypto/tls" "encoding/base64" "encoding/json" @@ -355,20 +354,12 @@ func (c *SiteReplicationSys) AddPeerClusters(ctx context.Context, sites []madmin // Generate a secret key for the service account. var secretKey string - { - secretKeyBuf := make([]byte, 40) - n, err := rand.Read(secretKeyBuf) - if err == nil && n != 40 { - err = fmt.Errorf("Unable to read 40 random bytes to generate secret key") + _, secretKey, err := auth.GenerateCredentials() + if err != nil { + return madmin.ReplicateAddStatus{}, SRError{ + Cause: err, + Code: ErrInternalError, } - if err != nil { - return madmin.ReplicateAddStatus{}, SRError{ - Cause: err, - Code: ErrInternalError, - } - } - secretKey = strings.Replace(string([]byte(base64.StdEncoding.EncodeToString(secretKeyBuf))[:40]), - "/", "+", -1) } svcCred, err := globalIAMSys.NewServiceAccount(ctx, sites[selfIdx].AccessKey, nil, newServiceAccountOpts{ @@ -1270,9 +1261,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") diff --git a/go.mod b/go.mod index 55301677f..ed5ac1cb8 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( github.com/minio/madmin-go v1.1.11-0.20211102182201-e51fd3d6b104 github.com/minio/minio-go/v7 v7.0.15 github.com/minio/parquet-go v1.0.0 - github.com/minio/pkg v1.1.5 + github.com/minio/pkg v1.1.6-0.20211103212545-951bbd71498c github.com/minio/selfupdate v0.3.1 github.com/minio/sha256-simd v1.0.0 github.com/minio/simdjson-go v0.2.1 diff --git a/go.sum b/go.sum index 3c025814a..19ba3cc22 100644 --- a/go.sum +++ b/go.sum @@ -1077,6 +1077,10 @@ github.com/minio/pkg v1.0.11/go.mod h1:32x/3OmGB0EOi1N+3ggnp+B5VFkSBBB9svPMVfpnf github.com/minio/pkg v1.1.3/go.mod h1:32x/3OmGB0EOi1N+3ggnp+B5VFkSBBB9svPMVfpnf14= github.com/minio/pkg v1.1.5 h1:phwKkJBQdVLyxOXC3RChPVGLtebplzQJ5jJ3l/HBvnk= github.com/minio/pkg v1.1.5/go.mod h1:32x/3OmGB0EOi1N+3ggnp+B5VFkSBBB9svPMVfpnf14= +github.com/minio/pkg v1.1.6-0.20211102234044-cd6b7b169e31 h1:nZkTtdcp4JgClBFI+mZJNO1J+8bEpcrOumdsbgdtF0A= +github.com/minio/pkg v1.1.6-0.20211102234044-cd6b7b169e31/go.mod h1:32x/3OmGB0EOi1N+3ggnp+B5VFkSBBB9svPMVfpnf14= +github.com/minio/pkg v1.1.6-0.20211103212545-951bbd71498c h1:zP0nEhOBjJRu6fP8nrNMUoGVZGIHbFKY1Ln5V/6Djbg= +github.com/minio/pkg v1.1.6-0.20211103212545-951bbd71498c/go.mod h1:32x/3OmGB0EOi1N+3ggnp+B5VFkSBBB9svPMVfpnf14= github.com/minio/selfupdate v0.3.1 h1:BWEFSNnrZVMUWXbXIgLDNDjbejkmpAmZvy/nCz1HlEs= github.com/minio/selfupdate v0.3.1/go.mod h1:b8ThJzzH7u2MkF6PcIra7KaXO9Khf6alWPvMSyTDCFM= github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=