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