// 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" "maps" "path" "sort" "strings" "sync" "time" jsoniter "github.com/json-iterator/go" "github.com/minio/madmin-go/v3" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/config/identity/openid" "github.com/minio/minio/internal/jwt" "github.com/pgsty/silo-pkg/v3/env" "github.com/pgsty/silo-pkg/v3/policy" "github.com/pgsty/silo-pkg/v3/sync/errgroup" "github.com/puzpuzpuz/xsync/v3" "golang.org/x/sync/singleflight" ) 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 minServiceAccountExpiry time.Duration = 15 * time.Minute maxServiceAccountExpiry time.Duration = 365 * 24 * time.Hour ) var errInvalidSvcAcctExpiration = errors.New("invalid service account expiration") 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 saveIAMFormat(ctx context.Context, store IAMStorageAPI) error { bootstrapTraceMsg("Load IAM format file") var iamFmt iamFormat path := getIAMFormatFilePath() if err := store.loadIAMConfig(ctx, &iamFmt, path); err != nil && !errors.Is(err, errConfigNotFound) { // if IAM format return err } if iamFmt.Version >= iamFormatVersion1 { // Nothing to do. return nil } bootstrapTraceMsg("Write IAM format file") // Save iam format to version 1. return store.saveIAMConfig(ctx, newIAMFormatVersion1(), path) } 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 { Deleted bool `json:"deleted,omitempty"` RevokedBefore time.Time `json:"revokedBefore,omitempty"` ExpiresAt time.Time `json:"expiresAt,omitempty"` Version int `json:"version"` Credentials auth.Credentials `json:"credentials"` UpdatedAt time.Time `json:"updatedAt"` } func newUserIdentity(cred auth.Credentials) UserIdentity { return UserIdentity{Version: 1, Credentials: cred, UpdatedAt: UTCNow()} } // GroupInfo contains info about a group type GroupInfo struct { RevokedBefore time.Time `json:"revokedBefore,omitempty"` MemberGrants map[string]time.Time `json:"memberGrants,omitempty"` Deleted bool `json:"deleted,omitempty"` Version int `json:"version"` Status string `json:"status"` Members []string `json:"members"` UpdatedAt time.Time `json:"updatedAt"` } func newGroupInfo(members []string) GroupInfo { return GroupInfo{Version: 1, Status: statusEnabled, Members: members, UpdatedAt: UTCNow()} } // MappedPolicy represents a policy name mapped to a user or group type MappedPolicy struct { Deleted bool `json:"deleted,omitempty"` Version int `json:"version"` Policies string `json:"policy"` UpdatedAt time.Time `json:"updatedAt"` } // mappedPoliciesToMap copies the map of mapped policies to a regular map. func mappedPoliciesToMap(m *xsync.MapOf[string, MappedPolicy]) map[string]MappedPolicy { policies := make(map[string]MappedPolicy, m.Size()) m.Range(func(k string, v MappedPolicy) bool { policies[k] = v return true }) return policies } // converts a mapped policy into a slice of distinct policies func (mp MappedPolicy) toSlice() []string { var policies []string for policy := range strings.SplitSeq(mp.Policies, ",") { if strings.TrimSpace(policy) == "" { continue } policies = append(policies, policy) } return policies } func (mp MappedPolicy) policySet() set.StringSet { return set.CreateStringSet(mp.toSlice()...) } func newMappedPolicy(policy string) MappedPolicy { return MappedPolicy{Version: 1, Policies: policy, UpdatedAt: UTCNow()} } // PolicyDoc represents an IAM policy with some metadata. type PolicyDoc struct { Deleted bool `json:"deleted,omitempty"` Version int `json:",omitempty"` Policy policy.Policy CreateDate time.Time UpdateDate time.Time } func newPolicyDoc(p policy.Policy) PolicyDoc { now := UTCNow().Round(time.Millisecond) return PolicyDoc{ Version: 1, Policy: p, CreateDate: now, UpdateDate: now, } } // defaultPolicyDoc - used to wrap a default policy as PolicyDoc. func defaultPolicyDoc(p policy.Policy) PolicyDoc { return PolicyDoc{ Version: 1, Policy: p, } } func (d *PolicyDoc) update(p policy.Policy) { now := UTCNow().Round(time.Millisecond) d.UpdateDate = now if d.CreateDate.IsZero() { d.CreateDate = now } d.Policy = p } // parseJSON parses both the old and the new format for storing policy // definitions. // // The on-disk format of policy definitions has changed (around early 12/2021) // from policy.Policy to PolicyDoc. To avoid a migration, loading supports // both the old and the new formats. func (d *PolicyDoc) parseJSON(data []byte) error { json := jsoniter.ConfigCompatibleWithStandardLibrary var doc PolicyDoc err := json.Unmarshal(data, &doc) if err != nil { err2 := json.Unmarshal(data, &doc.Policy) if err2 != nil { // Just return the first error. return err } d.Policy = doc.Policy return nil } *d = doc return nil } // 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 { updatedAt time.Time revisions *iamRevisionIndex // map of policy names to policy definitions iamPolicyDocsMap map[string]PolicyDoc // map of regular username to credentials iamUsersMap map[string]UserIdentity // map of regular username to policy names iamUserPolicyMap *xsync.MapOf[string, MappedPolicy] // STS accounts are loaded on demand and not via the periodic IAM reload. // map of STS access key to credentials iamSTSAccountsMap map[string]UserIdentity // map of STS access key to policy names iamSTSPolicyMap *xsync.MapOf[string, MappedPolicy] // 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 group names to policy names iamGroupPolicyMap *xsync.MapOf[string, MappedPolicy] } func newIamCache() *iamCache { return &iamCache{ iamPolicyDocsMap: map[string]PolicyDoc{}, iamUsersMap: map[string]UserIdentity{}, iamUserPolicyMap: xsync.NewMapOf[string, MappedPolicy](), iamSTSAccountsMap: map[string]UserIdentity{}, iamSTSPolicyMap: xsync.NewMapOf[string, MappedPolicy](), iamGroupsMap: map[string]GroupInfo{}, iamUserGroupMemberships: map[string]set.StringSet{}, iamGroupPolicyMap: xsync.NewMapOf[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 { if !c.groupMemberAllowed(member, gi.MemberGrants[member], gi.RevokedBefore) { continue } 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 } } func (c *iamCache) policyDBGetGroups(store *IAMStoreSys, userPolicyPresent bool, groups ...string) ([]string, error) { var policies []string for _, group := range groups { if store.getUsersSysType() == MinIOUsersSysType { g, ok := c.iamGroupsMap[group] if !ok { continue } // Group is disabled, so we return no policy - this // ensures the request is denied. if g.Status == statusDisabled { continue } } policy, ok := c.cachedMappedPolicy(group, regUser, true) if !ok { continue } policies = append(policies, policy.toSlice()...) } found := len(policies) > 0 if found { return policies, nil } if userPolicyPresent { // if user mapping present and no group policies found // rely on user policy for access, instead of fallback. return nil, nil } var mu sync.Mutex // no mappings found, fallback for all groups. g := errgroup.WithNErrs(len(groups)).WithConcurrency(10) // load like 10 groups at a time. for index := range groups { g.Go(func() error { err := store.loadMappedPolicy(context.TODO(), groups[index], regUser, true, c.iamGroupPolicyMap) if err != nil && !errors.Is(err, errNoSuchPolicy) { return err } if errors.Is(err, errNoSuchPolicy) { return nil } policy, _ := c.cachedMappedPolicy(groups[index], regUser, true) mu.Lock() policies = append(policies, policy.toSlice()...) mu.Unlock() return nil }, index) } err := errors.Join(g.Wait()...) return policies, err } // 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(store *IAMStoreSys, name string, isGroup bool, policyPresent bool) ([]string, time.Time, error) { if isGroup { if store.getUsersSysType() == MinIOUsersSysType { g, ok := c.iamGroupsMap[name] if !ok { if err := store.loadGroup(context.Background(), name, c.iamGroupsMap); err != nil { return nil, time.Time{}, err } g, ok = c.iamGroupsMap[name] if !ok { return nil, time.Time{}, errNoSuchGroup } } // Group is disabled, so we return no policy - this // ensures the request is denied. if g.Status == statusDisabled { return nil, time.Time{}, nil } } policy, ok := c.cachedMappedPolicy(name, regUser, true) if ok { return policy.toSlice(), policy.UpdatedAt, nil } if !policyPresent { if err := store.loadMappedPolicy(context.TODO(), name, regUser, true, c.iamGroupPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) { return nil, time.Time{}, err } policy, _ = c.cachedMappedPolicy(name, regUser, true) return policy.toSlice(), policy.UpdatedAt, nil } return nil, time.Time{}, nil } // returned policy could be empty, we use set to de-duplicate. var policies set.StringSet var updatedAt time.Time if store.getUsersSysType() == LDAPUsersSysType { // For LDAP policy mapping is part of STS users, we only need to lookup // those mappings. mp, ok := c.cachedMappedPolicy(name, stsUser, false) if !ok { // Attempt to load parent user mapping for STS accounts if err := store.loadMappedPolicy(context.TODO(), name, stsUser, false, c.iamSTSPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) { return nil, time.Time{}, err } mp, _ = c.cachedMappedPolicy(name, stsUser, false) } if !c.revisions.mappingAllowed(getMappedPolicyPath(name, stsUser, false), mp) { mp = MappedPolicy{} } policies = set.CreateStringSet(mp.toSlice()...) updatedAt = mp.UpdatedAt } else { // When looking for a user's policies, we also check if the user // and the groups they are member of are enabled. u, ok := c.iamUsersMap[name] if ok { if !u.Credentials.IsValid() { return nil, time.Time{}, nil } } // For internal IDP regular/service account user accounts, the policy // mapping is iamUserPolicyMap. For STS accounts, the parent user would be // passed here and we lookup the mapping in iamSTSPolicyMap. mp, ok := c.cachedMappedPolicy(name, regUser, false) if !ok { if err := store.loadMappedPolicy(context.TODO(), name, regUser, false, c.iamUserPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) { return nil, time.Time{}, err } mp, ok = c.cachedMappedPolicy(name, regUser, false) if !ok { // Since user "name" could be a parent user of an STS account, we look up // mappings for those too. mp, ok = c.cachedMappedPolicy(name, stsUser, false) if !ok { // Attempt to load parent user mapping for STS accounts if err := store.loadMappedPolicy(context.TODO(), name, stsUser, false, c.iamSTSPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) { return nil, time.Time{}, err } mp, _ = c.cachedMappedPolicy(name, stsUser, false) } } } policies = set.CreateStringSet(mp.toSlice()...) for _, group := range u.Credentials.Groups { g, ok := c.iamGroupsMap[group] if ok { // Group is disabled, so we return no policy - this // ensures the request is denied. if g.Status == statusDisabled { return nil, time.Time{}, nil } } policy, ok := c.cachedMappedPolicy(group, regUser, true) if !ok { if err := store.loadMappedPolicy(context.TODO(), group, regUser, true, c.iamGroupPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) { return nil, time.Time{}, err } policy, _ = c.cachedMappedPolicy(group, regUser, true) } for _, p := range policy.toSlice() { policies.Add(p) } } updatedAt = mp.UpdatedAt } for _, group := range c.effectiveUserGroups(name) { if store.getUsersSysType() == MinIOUsersSysType { g, ok := c.iamGroupsMap[group] if ok { // Group is disabled, so we return no policy - this // ensures the request is denied. if g.Status == statusDisabled { return nil, time.Time{}, nil } } } policy, ok := c.cachedMappedPolicy(group, regUser, true) if !ok { if err := store.loadMappedPolicy(context.TODO(), group, regUser, true, c.iamGroupPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) { return nil, time.Time{}, err } policy, _ = c.cachedMappedPolicy(group, regUser, true) } for _, p := range policy.toSlice() { policies.Add(p) } } return policies.ToSlice(), updatedAt, nil } func (c *iamCache) updateUserWithClaims(key string, u UserIdentity) error { if u.Credentials.SessionToken != "" { jwtClaims, err := extractJWTClaims(u) if err != nil { return err } u.Credentials.Claims = jwtClaims.Map() } if u.Credentials.IsTemp() && !u.Credentials.IsServiceAccount() { c.iamSTSAccountsMap[key] = u } else { c.iamUsersMap[key] = u } c.updatedAt = time.Now() return 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() getUsersSysType() UsersSysType loadPolicyDoc(ctx context.Context, policy string, m map[string]PolicyDoc) error loadPolicyDocWithRetry(ctx context.Context, policy string, m map[string]PolicyDoc, retries int) error loadPolicyDocs(ctx context.Context, m map[string]PolicyDoc) error loadUser(ctx context.Context, user string, userType IAMUserType, m map[string]UserIdentity) error loadSecretKey(ctx context.Context, user string, userType IAMUserType) (string, error) loadUsers(ctx context.Context, userType IAMUserType, m map[string]UserIdentity) 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 *xsync.MapOf[string, MappedPolicy]) error loadMappedPolicyWithRetry(ctx context.Context, name string, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy], retries int) error loadMappedPolicies(ctx context.Context, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy]) error saveIAMConfig(ctx context.Context, item any, path string, opts ...options) error loadIAMConfig(ctx context.Context, item any, path string) error deleteIAMConfig(ctx context.Context, path string) error listIAMConfigPaths(ctx context.Context) ([]string, error) revisionIndex() *iamRevisionIndex } // iamStorageWatcher is implemented by `IAMStorageAPI` implementers that // additionally support watching storage for changes. type iamStorageWatcher interface { watch(ctx context.Context, keyPath string) <-chan iamWatchEvent } // Set default canned policies only if not already overridden by users. func setDefaultCannedPolicies(policies map[string]PolicyDoc) { for _, v := range policy.DefaultPolicies { if _, ok := policies[v.Name]; !ok { policies[v.Name] = defaultPolicyDoc(v.Definition) } } } // LoadIAMCache reads all IAM items and populates a new iamCache object and // replaces the in-memory cache object. func (store *IAMStoreSys) LoadIAMCache(ctx context.Context, firstTime bool) error { ctx = withIAMExpirationCleanup(ctx) bootstrapTraceMsgFirstTime := func(s string) { if firstTime { bootstrapTraceMsg(s) } } bootstrapTraceMsgFirstTime("loading IAM data") newCache := newIamCache() newCache.revisions = store.revisionIndex() loadedAt := time.Now() if iamOS, ok := store.IAMStorageAPI.(*IAMObjectStore); ok { err := iamOS.loadAllFromObjStore(ctx, newCache, firstTime) if err != nil { return err } } else { // Only non-object IAM store (i.e. only etcd backend). bootstrapTraceMsgFirstTime("loading policy documents") 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 { bootstrapTraceMsgFirstTime("loading regular users") if err := store.loadUsers(ctx, regUser, newCache.iamUsersMap); err != nil { return err } bootstrapTraceMsgFirstTime("loading regular groups") if err := store.loadGroups(ctx, newCache.iamGroupsMap); err != nil { return err } } bootstrapTraceMsgFirstTime("loading user policy mapping") // load polices mapped to users if err := store.loadMappedPolicies(ctx, regUser, false, newCache.iamUserPolicyMap); err != nil { return err } // External-IDP mappings are durable authorization records even when // no local service account currently refers to their parent. if err := store.loadMappedPolicies(ctx, stsUser, false, newCache.iamSTSPolicyMap); err != nil { return err } bootstrapTraceMsgFirstTime("loading group policy mapping") // load policies mapped to groups if err := store.loadMappedPolicies(ctx, regUser, true, newCache.iamGroupPolicyMap); err != nil { return err } bootstrapTraceMsgFirstTime("loading service accounts") // load service accounts if err := store.loadUsers(ctx, svcUser, newCache.iamUsersMap); err != nil { return err } newCache.buildUserGroupMemberships() } for name := range newCache.iamPolicyDocsMap { if store.revisionIndex().get(getPolicyDocPath(name)).Deleted { delete(newCache.iamPolicyDocsMap, name) } } cache := store.lock() defer store.unlock() // We should only update the in-memory cache if there were no changes // to the in-memory cache since the disk loading began. If there // were changes to the in-memory cache we should wait for the next // cycle until we can safely update the in-memory cache. // // An in-memory cache must be replaced only if we know for sure that the // values loaded from disk are not stale. They might be stale if the // cached.updatedAt is more recent than the refresh cycle began. if cache.updatedAt.Before(loadedAt) || firstTime { // No one has updated anything since the config was loaded, // so we just replace whatever is on the disk into memory. cache.iamGroupPolicyMap = newCache.iamGroupPolicyMap cache.iamGroupsMap = newCache.iamGroupsMap cache.iamPolicyDocsMap = newCache.iamPolicyDocsMap cache.iamUserGroupMemberships = newCache.iamUserGroupMemberships cache.iamUserPolicyMap = newCache.iamUserPolicyMap cache.iamUsersMap = newCache.iamUsersMap // For STS policy map, we need to merge the new cache with the existing // cache because the periodic IAM reload is partial. The periodic load // here is to account for STS policy mapping changes that should apply // for service accounts derived from such STS accounts (i.e. LDAP STS // accounts). newCache.iamSTSPolicyMap.Range(func(k string, v MappedPolicy) bool { cache.iamSTSPolicyMap.Store(k, v) return true }) cache.updatedAt = time.Now() } return nil } // IAMStoreSys contains IAMStorageAPI to add higher-level methods on the storage // layer. type IAMStoreSys struct { IAMStorageAPI // Serialize cache writers independently of readers, so persistence can // release the cache lock without another writer invalidating its state. writeMu sync.Mutex group *singleflight.Group policy *singleflight.Group } func (store *IAMStoreSys) lock() *iamCache { store.writeMu.Lock() return store.IAMStorageAPI.lock() } func (store *IAMStoreSys) unlock() { store.IAMStorageAPI.unlock() store.writeMu.Unlock() } // 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) (UserIdentity, bool) { cache := store.rlock() defer store.runlock() u, ok := cache.iamUsersMap[user] if !ok { // Check the sts map u, ok = cache.iamSTSAccountsMap[user] } if ok && u.Credentials.ParentUser != "" { r := cache.userRevocation(u.Credentials.ParentUser) if r.Deleted || !iamCredentialSurvivesRevocation(u.Credentials, r.RevokedBefore) { return UserIdentity{}, false } } return u, 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.cachedMappedPolicy(name, regUser, true) return v, ok } return cache.cachedMappedPolicy(name, regUser, false) } // 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.lock() defer store.unlock() 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) cache.iamGroupPolicyMap.Delete(group) cache.updatedAt = time.Now() 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) cache.updatedAt = time.Now() return nil } // PolicyDBGet - fetches policies associated with the given user or group, and // additional groups if provided. func (store *IAMStoreSys) PolicyDBGet(name string, groups ...string) ([]string, error) { if name == "" { return nil, errInvalidArgument } cache := store.rlock() defer store.runlock() getPolicies := func() ([]string, error) { policies, _, err := cache.policyDBGet(store, name, false, false) if err != nil { return nil, err } userPolicyPresent := len(policies) > 0 groupPolicies, err := cache.policyDBGetGroups(store, userPolicyPresent, groups...) if err != nil { return nil, err } policies = append(policies, groupPolicies...) return policies, nil } if store.policy != nil { val, err, _ := store.policy.Do(name, func() (any, error) { return getPolicies() }) if err != nil { return nil, err } res, ok := val.([]string) if !ok { return nil, errors.New("unexpected policy type") } return res, nil } return getPolicies() } // AddUsersToGroup - adds users to group, creating the group if needed. func (store *IAMStoreSys) AddUsersToGroup(ctx context.Context, group string, members []string) (updatedAt time.Time, err error) { if group == "" { return updatedAt, errInvalidArgument } cache := store.lock() defer store.unlock() if err := store.checkIAMRevision(ctx, getGroupInfoPath(group), false); err != nil { return updatedAt, err } gi, ok := cache.iamGroupsMap[group] if !ok { // Set group as enabled by default when it doesn't // exist. gi = newGroupInfo(nil) } if r := store.revisionIndex().get(getGroupInfoPath(group)); r.RevokedBefore.After(gi.RevokedBefore) { gi.RevokedBefore = r.RevokedBefore } gi, err = cache.addGroupMembers(ctx, gi, members) if err != nil { return updatedAt, err } if err := store.saveGroupInfo(context.WithValue(ctx, iamGroupMutationKey{}, iamGroupMutation{Members: members}), group, &gi); err != nil { return updatedAt, err } cache.iamGroupsMap[group] = gi cache.removeGroupFromMembershipsMap(group) cache.updateGroupMembershipsMap(group, &gi) cache.updatedAt = time.Now() return gi.UpdatedAt, 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) (updatedAt time.Time, err error) { gi, ok := cache.iamGroupsMap[group] if !ok { return updatedAt, errNoSuchGroup } s := set.CreateStringSet(gi.Members...) d := set.CreateStringSet(members...) gi.Members = s.Difference(d).ToSlice() gi.UpdatedAt = UTCNow() if !updateCacheOnly { err := store.saveGroupInfo(context.WithValue(ctx, iamGroupMutationKey{}, iamGroupMutation{Members: members, Remove: true}), group, &gi) if err != nil { return updatedAt, 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 } cache.updatedAt = time.Now() return gi.UpdatedAt, nil } // RemoveUsersFromGroup - removes users from group, deleting it if it is empty. func (store *IAMStoreSys) RemoveUsersFromGroup(ctx context.Context, group string, members []string) (updatedAt time.Time, err error) { if group == "" { return updatedAt, errInvalidArgument } cache := store.lock() defer store.unlock() _, replicated := iamReplicationTime(ctx) if len(members) != 0 { if err := store.checkIAMRevision(ctx, getGroupInfoPath(group), false); err != nil { return updatedAt, err } } // Validate that all members exist. for _, member := range members { u, ok := cache.iamUsersMap[member] if !ok { return updatedAt, errNoSuchUser } cr := u.Credentials if cr.IsTemp() || cr.IsServiceAccount() { return updatedAt, errIAMActionNotAllowed } } gi, ok := cache.iamGroupsMap[group] if !ok && (!replicated || len(members) != 0) { return updatedAt, errNoSuchGroup } // Check if attempting to delete a non-empty group. if !replicated && len(members) == 0 && len(cache.effectiveGroupMembers(gi)) != 0 { return updatedAt, errGroupNotEmpty } if len(members) == 0 { // Commit the group revision before attempting dependent cleanup. A // newer mapping cannot veto a deletion or conceal an older boundary. op := &iamGroupRevocation{requireEmpty: !replicated} err := store.saveIAMRevision(ctx, getGroupInfoPath(group), op) retained := errors.Is(err, errIAMRevocationRetained) if err != nil && !retained { return updatedAt, err } updatedAt = op.RevokedBefore cache.removeGroupFromMembershipsMap(group) if retained { cache.iamGroupsMap[group] = op.GroupInfo cache.updateGroupMembershipsMap(group, &op.GroupInfo) } else { delete(cache.iamGroupsMap, group) } cache.updatedAt = time.Now() cleanupErr := store.deleteMappedPolicy(withIAMReplicationTime(ctx, updatedAt), group, regUser, true) if mp, ok := cache.iamGroupPolicyMap.Load(group); ok && !mp.UpdatedAt.After(updatedAt) { cache.iamGroupPolicyMap.Delete(group) } if cleanupErr != nil && !errors.Is(cleanupErr, errIAMStaleUpdate) { return updatedAt, &iamCommittedCleanupError{err: cleanupErr, retained: retained} } return updatedAt, nil } return removeMembersFromGroup(ctx, store, cache, group, members, false) } // SetGroupStatus - updates group status func (store *IAMStoreSys) SetGroupStatus(ctx context.Context, group string, enabled bool) (updatedAt time.Time, err error) { if group == "" { return updatedAt, errInvalidArgument } cache := store.lock() defer store.unlock() gi, ok := cache.iamGroupsMap[group] if !ok { return updatedAt, errNoSuchGroup } if enabled { gi.Status = statusEnabled } else { gi.Status = statusDisabled } gi.UpdatedAt = UTCNow() if err := store.saveGroupInfo(context.WithValue(ctx, iamGroupMutationKey{}, iamGroupMutation{StatusOnly: true}), group, &gi); err != nil { return gi.UpdatedAt, err } cache.iamGroupsMap[group] = gi cache.updatedAt = time.Now() return gi.UpdatedAt, nil } // GetGroupDescription - builds up group description func (store *IAMStoreSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err error) { cache := store.rlock() defer store.runlock() ps, updatedAt, err := cache.policyDBGet(store, group, true, false) if err != nil { return gd, err } policy := strings.Join(ps, ",") if store.getUsersSysType() != MinIOUsersSysType { return madmin.GroupDesc{ Name: group, Policy: policy, UpdatedAt: updatedAt, }, nil } gi, ok := cache.iamGroupsMap[group] if !ok { return gd, errNoSuchGroup } return madmin.GroupDesc{ Name: group, Status: gi.Status, Members: cache.effectiveGroupMembers(gi), Policy: policy, UpdatedAt: gi.UpdatedAt, }, nil } // updateGroups updates the group from the persistent store, and also related policy mapping if any. func (store *IAMStoreSys) updateGroups(ctx context.Context, cache *iamCache) (res []string, err error) { groupSet := set.NewStringSet() if iamOS, ok := store.IAMStorageAPI.(*IAMObjectStore); ok { listedConfigItems, err := iamOS.listAllIAMConfigItems(ctx) if err != nil { return nil, err } if store.getUsersSysType() == MinIOUsersSysType { groupsList := listedConfigItems[groupsListKey] for _, item := range groupsList { group := path.Dir(item) if err = iamOS.loadGroup(ctx, group, cache.iamGroupsMap); err != nil && !errors.Is(err, errNoSuchGroup) { return nil, fmt.Errorf("unable to load the group: %w", err) } if errors.Is(err, errNoSuchGroup) { delete(cache.iamGroupsMap, group) continue } groupSet.Add(group) } } groupPolicyMappingsList := listedConfigItems[policyDBGroupsListKey] for _, item := range groupPolicyMappingsList { group := strings.TrimSuffix(item, ".json") if err = iamOS.loadMappedPolicy(ctx, group, regUser, true, cache.iamGroupPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) { return nil, fmt.Errorf("unable to load the policy mapping for the group: %w", err) } if errors.Is(err, errNoSuchPolicy) { cache.iamGroupPolicyMap.Delete(group) continue } groupSet.Add(group) } return groupSet.ToSlice(), nil } // For etcd just return from cache. for k := range cache.iamGroupsMap { groupSet.Add(k) } cache.iamGroupPolicyMap.Range(func(k string, v MappedPolicy) bool { groupSet.Add(k) return true }) return groupSet.ToSlice(), 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.lock() defer store.unlock() return store.updateGroups(ctx, cache) } // listGroups - lists groups - fetch groups from cache func (store *IAMStoreSys) listGroups(ctx context.Context) (res []string, err error) { cache := store.rlock() defer store.runlock() if store.getUsersSysType() == MinIOUsersSysType { for k := range cache.iamGroupsMap { res = append(res, k) } } if store.getUsersSysType() == LDAPUsersSysType { cache.iamGroupPolicyMap.Range(func(k string, _ MappedPolicy) bool { res = append(res, k) return true }) } return res, err } // PolicyDBUpdate - adds or removes given policies to/from the user or group's // policy associations. func (store *IAMStoreSys) PolicyDBUpdate(ctx context.Context, name string, isGroup bool, userType IAMUserType, policies []string, isAttach bool) (updatedAt time.Time, addedOrRemoved, effectivePolicies []string, err error, ) { if name == "" { err = errInvalidArgument return updatedAt, addedOrRemoved, effectivePolicies, err } cache := store.lock() defer store.unlock() // Load existing policy mapping var mp MappedPolicy if !isGroup { if userType == stsUser { stsMap := xsync.NewMapOf[string, MappedPolicy]() // Attempt to load parent user mapping for STS accounts store.loadMappedPolicy(context.TODO(), name, stsUser, false, stsMap) mp, _ = stsMap.Load(name) } else { mp, _ = cache.iamUserPolicyMap.Load(name) } } else { if store.getUsersSysType() == MinIOUsersSysType { g, ok := cache.iamGroupsMap[name] if !ok { err = errNoSuchGroup return updatedAt, addedOrRemoved, effectivePolicies, err } if g.Status == statusDisabled { err = errGroupDisabled return updatedAt, addedOrRemoved, effectivePolicies, err } } mp, _ = cache.iamGroupPolicyMap.Load(name) } // Compute net policy change effect and updated policy mapping existingPolicySet := mp.policySet() policiesToUpdate := set.CreateStringSet(policies...) var newPolicySet set.StringSet newPolicyMapping := mp if isAttach { // new policies to attach => inputPolicies - existing (set difference) policiesToUpdate = policiesToUpdate.Difference(existingPolicySet) // validate that new policies to add are defined. for _, p := range policiesToUpdate.ToSlice() { if _, found := cache.iamPolicyDocsMap[p]; !found { err = errNoSuchPolicy return updatedAt, addedOrRemoved, effectivePolicies, err } } newPolicySet = existingPolicySet.Union(policiesToUpdate) } else { // policies to detach => inputPolicies ∩ existing (intersection) policiesToUpdate = policiesToUpdate.Intersection(existingPolicySet) newPolicySet = existingPolicySet.Difference(policiesToUpdate) } // We return an error if the requested policy update will have no effect. if policiesToUpdate.IsEmpty() { err = errNoPolicyToAttachOrDetach return updatedAt, addedOrRemoved, effectivePolicies, err } newPolicies := newPolicySet.ToSlice() newPolicyMapping.Policies = strings.Join(newPolicies, ",") newPolicyMapping.UpdatedAt = UTCNow() addedOrRemoved = policiesToUpdate.ToSlice() // In case of detach operation, it is possible that no policies are mapped - // in this case, we delete the mapping from the store. if len(newPolicies) == 0 { newPolicyMapping.Deleted = true if err = store.saveMappedPolicy(ctx, name, userType, isGroup, &newPolicyMapping); err != nil && !errors.Is(err, errNoSuchPolicy) { return updatedAt, addedOrRemoved, effectivePolicies, err } if !isGroup { if userType == stsUser { cache.iamSTSPolicyMap.Delete(name) } else { cache.iamUserPolicyMap.Delete(name) } } else { cache.iamGroupPolicyMap.Delete(name) } } else { if err = store.saveMappedPolicy(ctx, name, userType, isGroup, &newPolicyMapping); err != nil { return updatedAt, addedOrRemoved, effectivePolicies, err } if !isGroup { if userType == stsUser { cache.iamSTSPolicyMap.Store(name, newPolicyMapping) } else { cache.iamUserPolicyMap.Store(name, newPolicyMapping) } } else { cache.iamGroupPolicyMap.Store(name, newPolicyMapping) } } cache.updatedAt = UTCNow() return newPolicyMapping.UpdatedAt, addedOrRemoved, newPolicies, nil } // PolicyDBSet - update the policy mapping for the given user or group in // storage and in cache. We do not check for the existence of the user here // since users can be virtual, such as for: // - LDAP users // - CommonName for STS accounts generated by AssumeRoleWithCertificate func (store *IAMStoreSys) PolicyDBSet(ctx context.Context, name, policy string, userType IAMUserType, isGroup bool) (updatedAt time.Time, err error) { if name == "" { return updatedAt, errInvalidArgument } cache := store.lock() defer store.unlock() if err := store.checkIAMRevision(ctx, getMappedPolicyPath(name, userType, isGroup), policy == ""); err != nil { return updatedAt, err } // 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) } mp := MappedPolicy{Version: 1, Deleted: true} err := store.saveMappedPolicy(ctx, name, userType, isGroup, &mp) if err != nil && !errors.Is(err, errNoSuchPolicy) { return updatedAt, err } if !isGroup { if userType == stsUser { cache.iamSTSPolicyMap.Delete(name) } else { cache.iamUserPolicyMap.Delete(name) } } else { cache.iamGroupPolicyMap.Delete(name) } cache.updatedAt = time.Now() return mp.UpdatedAt, nil } // Handle policy mapping set/update mp := newMappedPolicy(policy) for _, p := range mp.toSlice() { if _, found := cache.iamPolicyDocsMap[p]; !found { return updatedAt, errNoSuchPolicy } } if err := store.saveMappedPolicy(ctx, name, userType, isGroup, &mp); err != nil { return updatedAt, err } if !isGroup { if userType == stsUser { cache.iamSTSPolicyMap.Store(name, mp) } else { cache.iamUserPolicyMap.Store(name, mp) } } else { cache.iamGroupPolicyMap.Store(name, mp) } cache.updatedAt = time.Now() return mp.UpdatedAt, 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.lock() defer store.unlock() err := store.loadPolicyDoc(ctx, policy, cache.iamPolicyDocsMap) if errors.Is(err, errNoSuchPolicy) { // policy was deleted, update cache. delete(cache.iamPolicyDocsMap, policy) // update user policy map cache.iamUserPolicyMap.Range(func(u string, mp MappedPolicy) bool { pset := mp.policySet() if !pset.Contains(policy) { return true } if store.getUsersSysType() == MinIOUsersSysType { _, ok := cache.iamUsersMap[u] if !ok { // happens when account is deleted or // expired. cache.iamUserPolicyMap.Delete(u) return true } } pset.Remove(policy) cache.iamUserPolicyMap.Store(u, newMappedPolicy(strings.Join(pset.ToSlice(), ","))) return true }) // update group policy map cache.iamGroupPolicyMap.Range(func(g string, mp MappedPolicy) bool { pset := mp.policySet() if !pset.Contains(policy) { return true } pset.Remove(policy) cache.iamGroupPolicyMap.Store(g, newMappedPolicy(strings.Join(pset.ToSlice(), ","))) return true }) cache.updatedAt = time.Now() return nil } return err } // DeletePolicy - deletes policy from storage and cache. When this called in // response to a notification (i.e. isFromNotification = true), it skips the // validation of policy usage and the attempt to delete in the backend as well // (as this is already done by the notifying node). func (store *IAMStoreSys) DeletePolicy(ctx context.Context, policy string, isFromNotification bool) error { if policy == "" { return errInvalidArgument } cache := store.lock() defer store.unlock() if !isFromNotification { if err := store.checkIAMRevision(ctx, getPolicyDocPath(policy), true); err != nil { return err } if _, replicated := iamReplicationTime(ctx); !replicated { if _, exists := cache.iamPolicyDocsMap[policy]; !exists { // Preserve idempotent local deletion without creating a durable // revocation for a misspelled/nonexistent policy name. return nil } // Check if policy is mapped to any existing user or group. If so, we do not // allow deletion of the policy. If the policy is mapped to an STS account, // we do allow deletion. users := []string{} groups := []string{} cache.iamUserPolicyMap.Range(func(u string, mp MappedPolicy) bool { pset := mp.policySet() if store.getUsersSysType() == MinIOUsersSysType { if _, ok := cache.iamUsersMap[u]; !ok { // This case can happen when a temporary account is // deleted or expired - remove it from userPolicyMap. cache.iamUserPolicyMap.Delete(u) return true } } if pset.Contains(policy) { users = append(users, u) } return true }) cache.iamGroupPolicyMap.Range(func(g string, mp MappedPolicy) bool { pset := mp.policySet() if pset.Contains(policy) { groups = append(groups, g) } return true }) if len(users) != 0 || len(groups) != 0 { return errPolicyInUse } } err := store.deletePolicyDoc(ctx, policy) if errors.Is(err, errNoSuchPolicy) { // Ignore error if policy is already deleted. err = nil } if err != nil { return err } } delete(cache.iamPolicyDocsMap, policy) cache.updatedAt = time.Now() return nil } // GetPolicy - gets the policy definition. Allows specifying multiple comma // separated policies - returns a combined policy. func (store *IAMStoreSys) GetPolicy(name string) (policy.Policy, error) { if name == "" { return policy.Policy{}, errInvalidArgument } cache := store.rlock() defer store.runlock() policies := newMappedPolicy(name).toSlice() var toMerge []policy.Policy for _, policy := range policies { if policy == "" { continue } v, ok := cache.iamPolicyDocsMap[policy] if !ok { return v.Policy, errNoSuchPolicy } toMerge = append(toMerge, v.Policy) } if len(toMerge) == 0 { return policy.Policy{}, errNoSuchPolicy } return policy.MergePolicies(toMerge...), nil } // GetPolicyDoc - gets the policy doc which has the policy and some metadata. // Exactly one policy must be specified here. func (store *IAMStoreSys) GetPolicyDoc(name string) (r PolicyDoc, err error) { name = strings.TrimSpace(name) if name == "" { return r, errInvalidArgument } cache := store.rlock() defer store.runlock() v, ok := cache.iamPolicyDocsMap[name] if !ok { return r, errNoSuchPolicy } return v, nil } // SetPolicy - creates a policy with name. func (store *IAMStoreSys) SetPolicy(ctx context.Context, name string, policy policy.Policy) (time.Time, error) { if policy.IsEmpty() || name == "" { return time.Time{}, errInvalidArgument } cache := store.lock() defer store.unlock() var ( d PolicyDoc ok bool ) if d, ok = cache.iamPolicyDocsMap[name]; ok { d.update(policy) } else { d = newPolicyDoc(policy) } if err := store.savePolicyDoc(ctx, name, &d); err != nil { return d.UpdateDate, err } cache.iamPolicyDocsMap[name] = d cache.updatedAt = time.Now() return d.UpdateDate, 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]policy.Policy, error) { cache := store.lock() defer store.unlock() m := map[string]PolicyDoc{} err := store.loadPolicyDocs(ctx, m) if err != nil { return nil, err } // Sets default canned policies setDefaultCannedPolicies(m) cache.iamPolicyDocsMap = m cache.updatedAt = time.Now() ret := map[string]policy.Policy{} for k, v := range m { if bucketName == "" || v.Policy.MatchResource(bucketName) { ret[k] = v.Policy } } return ret, nil } // ListPolicyDocs - fetches all policy docs from storage and updates cache as well. // If bucketName is non-empty, returns policy docs matching the bucket. func (store *IAMStoreSys) ListPolicyDocs(ctx context.Context, bucketName string) (map[string]PolicyDoc, error) { cache := store.lock() defer store.unlock() m := map[string]PolicyDoc{} err := store.loadPolicyDocs(ctx, m) if err != nil { return nil, err } // Sets default canned policies setDefaultCannedPolicies(m) cache.iamPolicyDocsMap = m cache.updatedAt = time.Now() ret := map[string]PolicyDoc{} for k, v := range m { if bucketName == "" || v.Policy.MatchResource(bucketName) { ret[k] = v } } return ret, nil } // fetches all policy docs from cache. // If bucketName is non-empty, returns policy docs matching the bucket. func (store *IAMStoreSys) listPolicyDocs(ctx context.Context, bucketName string) (map[string]PolicyDoc, error) { cache := store.rlock() defer store.runlock() ret := map[string]PolicyDoc{} for k, v := range cache.iamPolicyDocsMap { if bucketName == "" || v.Policy.MatchResource(bucketName) { ret[k] = v } } return ret, nil } // helper function - does not take locks. func filterPolicies(cache *iamCache, policyName string, bucketName string) (string, policy.Policy) { var policies []string mp := newMappedPolicy(policyName) var toMerge []policy.Policy for _, policy := range mp.toSlice() { if policy == "" { continue } p, found := cache.iamPolicyDocsMap[policy] if !found { continue } if bucketName == "" || p.Policy.MatchResource(bucketName) { policies = append(policies, policy) toMerge = append(toMerge, p.Policy) } } return strings.Join(policies, ","), policy.MergePolicies(toMerge...) } // MergePolicies - accepts a comma separated list of policy names as a string // and returns only policies that currently exist in MinIO. It includes hot loading // of policies if not in the memory func (store *IAMStoreSys) MergePolicies(policyName string) (string, policy.Policy) { var policies []string var missingPolicies []string var toMerge []policy.Policy cache := store.rlock() for _, policy := range newMappedPolicy(policyName).toSlice() { if policy == "" { continue } p, found := cache.iamPolicyDocsMap[policy] if !found { missingPolicies = append(missingPolicies, policy) continue } policies = append(policies, policy) toMerge = append(toMerge, p.Policy) } store.runlock() if len(missingPolicies) > 0 { m := make(map[string]PolicyDoc) for _, policy := range missingPolicies { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) _ = store.loadPolicyDoc(ctx, policy, m) cancel() } cache := store.lock() maps.Copy(cache.iamPolicyDocsMap, m) store.unlock() for policy, p := range m { policies = append(policies, policy) toMerge = append(toMerge, p.Policy) } } return strings.Join(policies, ","), policy.MergePolicies(toMerge...) } // 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 { c := v.Credentials if c.IsTemp() || c.IsServiceAccount() { continue } var policies []string mp, ok := cache.cachedMappedPolicy(k, regUser, false) if ok { policies = append(policies, mp.Policies) for _, group := range cache.effectiveUserGroups(k) { if nmp, ok := cache.cachedMappedPolicy(group, regUser, true); 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 c.IsValid() { return madmin.AccountEnabled } return madmin.AccountDisabled }(), MemberOf: cache.effectiveUserGroups(k), } } } 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, u := range cache.iamUsersMap { v := u.Credentials if v.IsTemp() || v.IsServiceAccount() { continue } pl, _ := cache.cachedMappedPolicy(k, regUser, false) result[k] = madmin.UserInfo{ PolicyName: pl.Policies, Status: func() madmin.AccountStatus { if v.IsValid() { return madmin.AccountEnabled } return madmin.AccountDisabled }(), MemberOf: cache.effectiveUserGroups(k), UpdatedAt: pl.UpdatedAt, } } return result } // GetUsersWithMappedPolicies - safely returns the name of access keys with associated policies func (store *IAMStoreSys) GetUsersWithMappedPolicies() map[string]string { cache := store.rlock() defer store.runlock() result := make(map[string]string) cache.iamUserPolicyMap.Range(func(k string, v MappedPolicy) bool { result[k] = v.Policies return true }) cache.iamSTSPolicyMap.Range(func(k string, v MappedPolicy) bool { result[k] = v.Policies return true }) 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.Credentials.ParentUser == name { groups = v.Credentials.Groups break } } for _, v := range cache.iamSTSAccountsMap { if v.Credentials.ParentUser == name { groups = v.Credentials.Groups break } } mappedPolicy, ok := cache.cachedMappedPolicy(name, regUser, false) if !ok { mappedPolicy, ok = cache.cachedMappedPolicy(name, stsUser, false) } if !ok { // Attempt to load parent user mapping for STS accounts store.loadMappedPolicy(context.TODO(), name, stsUser, false, cache.iamSTSPolicyMap) mappedPolicy, ok = cache.cachedMappedPolicy(name, stsUser, false) if !ok { return u, errNoSuchUser } } return madmin.UserInfo{ PolicyName: mappedPolicy.Policies, MemberOf: groups, UpdatedAt: mappedPolicy.UpdatedAt, }, nil } ui, found := cache.iamUsersMap[name] if !found { return u, errNoSuchUser } cred := ui.Credentials if cred.IsTemp() || cred.IsServiceAccount() { return u, errIAMActionNotAllowed } pl, _ := cache.cachedMappedPolicy(name, regUser, false) return madmin.UserInfo{ PolicyName: pl.Policies, Status: func() madmin.AccountStatus { if cred.IsValid() { return madmin.AccountEnabled } return madmin.AccountDisabled }(), MemberOf: cache.effectiveUserGroups(name), UpdatedAt: ui.UpdatedAt, }, 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.lock() defer store.unlock() var m *xsync.MapOf[string, MappedPolicy] switch { case isGroup: m = cache.iamGroupPolicyMap case userType == stsUser: m = cache.iamSTSPolicyMap default: m = cache.iamUserPolicyMap } err := store.loadMappedPolicy(ctx, userOrGroup, userType, isGroup, m) if errors.Is(err, errNoSuchPolicy) { // This means that the policy mapping was deleted, so we update // the cache. m.Delete(userOrGroup) cache.updatedAt = time.Now() err = nil } return err } // UserDeletionNotificationHandler refreshes all cached state affected by a // parent deletion. Reloading only a recreated parent would retain the older // children, policy, and group memberships in this node's cache. func (store *IAMStoreSys) UserDeletionNotificationHandler(ctx context.Context, accessKey string) error { if accessKey == "" { return errInvalidArgument } cache := store.rlock() groups := cache.iamUserGroupMemberships[accessKey].ToSlice() children := make(map[string]IAMUserType) for key, u := range cache.iamUsersMap { if u.Credentials.ParentUser == accessKey && u.Credentials.IsServiceAccount() { children[key] = svcUser } } store.runlock() if err := store.UserNotificationHandler(ctx, accessKey, regUser); err != nil { return err } if err := store.PolicyMappingNotificationHandler(ctx, accessKey, false, regUser); err != nil { return err } // Temporary credentials are loaded on demand. Invalidate their cached // copies; a later request must revalidate them against persisted state. cache = store.lock() for key, u := range cache.iamSTSAccountsMap { if u.Credentials.ParentUser == accessKey { delete(cache.iamSTSAccountsMap, key) } } cache.updatedAt = time.Now() store.unlock() for key, userType := range children { if err := store.UserNotificationHandler(ctx, key, userType); err != nil { return err } } for _, group := range groups { if err := store.GroupNotificationHandler(ctx, group); err != nil { return err } } return nil } // 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.lock() defer store.unlock() var m map[string]UserIdentity switch userType { case stsUser: m = cache.iamSTSAccountsMap default: m = cache.iamUsersMap } err := store.loadUser(ctx, accessKey, userType, m) if err == errNoSuchUser { // User was deleted - we update the cache. delete(m, accessKey) // Since cache was updated, we update the timestamp. defer func() { cache.updatedAt = time.Now() }() // 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 k, u := range cache.iamUsersMap { if u.Credentials.IsServiceAccount() && u.Credentials.ParentUser == accessKey { delete(cache.iamUsersMap, k) } } for k, u := range cache.iamSTSAccountsMap { if u.Credentials.ParentUser == accessKey { delete(cache.iamSTSAccountsMap, k) } } } // 3. Delete any mapped policy cache.iamUserPolicyMap.Delete(accessKey) return nil } if err != nil { return err } // Since cache was updated, we update the timestamp. defer func() { cache.updatedAt = time.Now() }() cred := m[accessKey].Credentials switch userType { case stsUser: // For STS accounts a policy is mapped to the parent user (if a mapping exists). err = store.loadMappedPolicy(ctx, cred.ParentUser, userType, false, cache.iamSTSPolicyMap) case svcUser: // For service accounts, the parent may be a regular (internal) IDP // user or a "virtual" user (parent of an STS account). // // If parent is a regular user => policy mapping is done on that parent itself. // // If parent is "virtual" => policy mapping is done on the virtual // parent and that virtual parent is an stsUser. // // To load the appropriate mapping, we check the parent user type. _, parentIsRegularUser := cache.iamUsersMap[cred.ParentUser] if parentIsRegularUser { err = store.loadMappedPolicy(ctx, cred.ParentUser, regUser, false, cache.iamUserPolicyMap) } else { err = store.loadMappedPolicy(ctx, cred.ParentUser, stsUser, false, cache.iamSTSPolicyMap) } case regUser: // For regular users, we load the mapped policy. err = store.loadMappedPolicy(ctx, accessKey, userType, false, cache.iamUserPolicyMap) default: // This is just to ensure that we have covered all cases for new // code in future. panic("unknown user type") } // Ignore policy not mapped error if err != nil && !errors.Is(err, errNoSuchPolicy) { return err } 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() // The identity record is the commit point. A failure in later cleanup // cannot undo its durable revocation or resurrect old dependent grants. var identity UserIdentity var err error retained := false if userType == regUser { op := &iamUserRevocation{} err = store.saveIAMRevision(ctx, getUserIdentityPath(accessKey, userType), op) identity, retained = op.UserIdentity, errors.Is(err, errIAMRevocationRetained) } else { identity = UserIdentity{Version: 1, Deleted: true} err = store.saveUserIdentity(ctx, accessKey, userType, &identity) } if err != nil && !retained { return err } if retained { cache.iamUsersMap[accessKey] = identity } else { delete(cache.iamUsersMap, accessKey) delete(cache.iamSTSAccountsMap, accessKey) } cache.updatedAt = time.Now() if store.group != nil { store.group.Forget(accessKey) } boundary := identity.RevokedBefore if boundary.IsZero() { boundary = identity.UpdatedAt } cleanupCtx := withIAMReplicationTime(ctx, boundary) // Membership is derived from the grant and parent revocation versions. // Do not rewrite a whole group as a side effect of deleting one member. for group, gi := range cache.iamGroupsMap { cache.removeGroupFromMembershipsMap(group) cache.updateGroupMembershipsMap(group, &gi) } var cleanupErrors []error if userType == regUser { for _, users := range []map[string]UserIdentity{cache.iamUsersMap, cache.iamSTSAccountsMap} { for key, child := range users { if child.Credentials.ParentUser != accessKey || iamCredentialSurvivesRevocation(child.Credentials, boundary) { continue } delete(users, key) if store.group != nil { store.group.Forget(key) } // The durable parent boundary suffices for STS. Avoid one // permanent tombstone per short-lived random access key. if child.Credentials.IsTemp() { continue } if child.Credentials.IsServiceAccount() { childCtx, _ := iamChildDeletionContext(cleanupCtx, child) if err := store.deleteUserIdentity(childCtx, key, svcUser); err != nil && !errors.Is(err, errIAMStaleUpdate) { cleanupErrors = append(cleanupErrors, err) } } } } } // A mapping newer than the revocation is an explicit new grant. Keep it. if userType != stsUser { // STS policies belong to the parent, not each random token key. if err := store.deleteMappedPolicy(cleanupCtx, accessKey, userType, false); err != nil && !errors.Is(err, errIAMStaleUpdate) { cleanupErrors = append(cleanupErrors, err) } } if mp, ok := cache.iamUserPolicyMap.Load(accessKey); ok && !mp.UpdatedAt.After(boundary) { cache.iamUserPolicyMap.Delete(accessKey) } if err := errors.Join(cleanupErrors...); err != nil { return &iamCommittedCleanupError{err: err, retained: retained} } if retained { return errIAMRevocationRetained } return nil } // SetTempUser - saves temporary (STS) credential to storage and cache. If a // policy name is given, it is associated with the parent user specified in the // credential. func (store *IAMStoreSys) SetTempUser(ctx context.Context, accessKey string, cred auth.Credentials, policyName string) (time.Time, error) { if accessKey == "" || !cred.IsTemp() || cred.IsExpired() || cred.ParentUser == "" { return time.Time{}, errInvalidArgument } ttl := int64(cred.Expiration.Sub(UTCNow()).Seconds()) cache := store.lock() defer store.unlock() if err := store.checkIAMRevision(ctx, getUserIdentityPath(accessKey, stsUser), false); err != nil { return time.Time{}, err } u := newUserIdentity(cred) claims, err := extractJWTClaims(u) if err != nil { return time.Time{}, err } u.Credentials.Claims = claims.Map() if err := store.checkIAMParentRevision(ctx, u.Credentials); err != nil { return time.Time{}, err } if policyName != "" { mp := newMappedPolicy(policyName) _, combinedPolicyStmt := filterPolicies(cache, mp.Policies, "") if combinedPolicyStmt.IsEmpty() { return time.Time{}, fmt.Errorf("specified policy %s, not found %w", policyName, errNoSuchPolicy) } err := store.saveMappedPolicy(ctx, cred.ParentUser, stsUser, false, &mp, options{ttl: ttl}) if err != nil { return time.Time{}, err } cache.iamSTSPolicyMap.Store(cred.ParentUser, mp) } err = store.saveUserIdentity(ctx, accessKey, stsUser, &u, options{ttl: ttl}) if err != nil { return time.Time{}, err } cache.iamSTSAccountsMap[accessKey] = u cache.updatedAt = time.Now() return u.UpdatedAt, nil } // RevokeTokens - revokes all temporary credentials, or those with matching type, // associated with the parent user. func (store *IAMStoreSys) RevokeTokens(ctx context.Context, parentUser string, tokenRevokeType string) error { if parentUser == "" { return errInvalidArgument } cache := store.lock() defer store.unlock() secret, err := getTokenSigningKey() if err != nil { return err } var revoked bool for _, ui := range cache.iamSTSAccountsMap { if ui.Credentials.ParentUser != parentUser { continue } if tokenRevokeType != "" { claims, err := getClaimsFromTokenWithSecret(ui.Credentials.SessionToken, secret) if err != nil { continue // skip if token is invalid } // skip if token type is given and does not match if v, _ := claims.Lookup(tokenRevokeTypeClaim); v != tokenRevokeType { continue } } if err := store.deleteUserIdentity(ctx, ui.Credentials.AccessKey, stsUser); err != nil { return err } delete(cache.iamSTSAccountsMap, ui.Credentials.AccessKey) revoked = true } if revoked { cache.updatedAt = time.Now() } 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.rlock() usersToDelete := set.CreateStringSet(users...) targets := make(map[string]IAMUserType) for _, identities := range []map[string]UserIdentity{cache.iamUsersMap, cache.iamSTSAccountsMap} { for key, ui := range identities { cred := ui.Credentials if !usersToDelete.Contains(key) && !usersToDelete.Contains(cred.ParentUser) { continue } typ := regUser if cred.IsServiceAccount() { typ = svcUser } else if cred.IsTemp() { typ = stsUser } targets[key] = typ } } store.runlock() var failures []error for key, typ := range targets { // External-IDP disablement is an early revocation, not natural token // expiration. Preserve the credential's durable ordering boundary. if err := store.DeleteUser(ctx, key, typ); err != nil && !errors.Is(err, errIAMRevocationRetained) { failures = append(failures, err) } } return errors.Join(failures...) } // ParentUserInfo contains extra info about a the parent user. type ParentUserInfo struct { subClaimValue string roleArns set.StringSet } // GetAllParentUsers - returns all distinct "parent-users" associated with STS // or service credentials, mapped to all distinct roleARNs associated with the // parent user. The dummy role ARN is associated with parent users from // policy-claim based OpenID providers. The root credential as a parent // user is not included in the result. func (store *IAMStoreSys) GetAllParentUsers() map[string]ParentUserInfo { cache := store.rlock() defer store.runlock() return store.getParentUsers(cache) } // assumes store is locked by caller. func (store *IAMStoreSys) getParentUsers(cache *iamCache) map[string]ParentUserInfo { res := map[string]ParentUserInfo{} for _, ui := range cache.iamUsersMap { cred := ui.Credentials // Only consider service account or STS credentials with // non-empty session tokens. if (!cred.IsServiceAccount() && !cred.IsTemp()) || cred.SessionToken == "" { continue } var ( err error claims *jwt.MapClaims ) if cred.IsServiceAccount() { claims, err = getClaimsFromTokenWithSecret(cred.SessionToken, cred.SecretKey) } else if cred.IsTemp() { var secretKey string secretKey, err = getTokenSigningKey() if err != nil { continue } claims, err = getClaimsFromTokenWithSecret(cred.SessionToken, secretKey) } if err != nil { continue } if cred.ParentUser == "" || cred.ParentUser == globalActiveCred.AccessKey { continue } subClaimValue := cred.ParentUser if v, ok := claims.Lookup(subClaim); ok { subClaimValue = v } if v, ok := claims.Lookup(ldapActualUser); ok { subClaimValue = v } roleArn := openid.DummyRoleARN.String() s, ok := claims.Lookup(roleArnClaim) if ok { roleArn = s } v, ok := res[cred.ParentUser] if ok { res[cred.ParentUser] = ParentUserInfo{ subClaimValue: subClaimValue, roleArns: v.roleArns.Union(set.CreateStringSet(roleArn)), } } else { res[cred.ParentUser] = ParentUserInfo{ subClaimValue: subClaimValue, roleArns: set.CreateStringSet(roleArn), } } } return res } // GetAllSTSUserMappings - Loads all STS user policy mappings from storage and // returns them. Also gets any STS users that do not have policy mappings but have // Service Accounts or STS keys (This is useful if the user is part of a group) func (store *IAMStoreSys) GetAllSTSUserMappings(userPredicate func(string) bool) (map[string]string, error) { cache := store.rlock() defer store.runlock() stsMap := make(map[string]string) m := xsync.NewMapOf[string, MappedPolicy]() if err := store.loadMappedPolicies(context.Background(), stsUser, false, m); err != nil { return nil, err } m.Range(func(user string, mappedPolicy MappedPolicy) bool { if userPredicate != nil && !userPredicate(user) { return true } stsMap[user] = mappedPolicy.Policies return true }) for user := range store.getParentUsers(cache) { if _, ok := stsMap[user]; !ok { if userPredicate != nil && !userPredicate(user) { continue } stsMap[user] = "" } } return stsMap, nil } // Assumes store is locked by caller. If userMap is empty, returns all user mappings. func (store *IAMStoreSys) listUserPolicyMappings(cache *iamCache, userMap map[string]set.StringSet, userPredicate func(string) bool, decodeFunc func(string) string, ) []madmin.UserPolicyEntities { stsMap := xsync.NewMapOf[string, MappedPolicy]() resMap := make(map[string]madmin.UserPolicyEntities, len(userMap)) for user, groupSet := range userMap { // Attempt to load parent user mapping for STS accounts store.loadMappedPolicy(context.TODO(), user, stsUser, false, stsMap) decodeUser := user if decodeFunc != nil { decodeUser = decodeFunc(user) } blankEntities := madmin.UserPolicyEntities{User: decodeUser} if !groupSet.IsEmpty() { blankEntities.MemberOfMappings = store.listGroupPolicyMappings(cache, groupSet, nil, decodeFunc) } resMap[user] = blankEntities } var r []madmin.UserPolicyEntities cache.iamUserPolicyMap.Range(func(user string, mappedPolicy MappedPolicy) bool { if userPredicate != nil && !userPredicate(user) { return true } entitiesWithMemberOf, ok := resMap[user] if !ok { if len(userMap) > 0 { return true } decodeUser := user if decodeFunc != nil { decodeUser = decodeFunc(user) } entitiesWithMemberOf = madmin.UserPolicyEntities{User: decodeUser} } ps := mappedPolicy.toSlice() sort.Strings(ps) entitiesWithMemberOf.Policies = ps resMap[user] = entitiesWithMemberOf return true }) stsMap.Range(func(user string, mappedPolicy MappedPolicy) bool { if userPredicate != nil && !userPredicate(user) { return true } entitiesWithMemberOf := resMap[user] ps := mappedPolicy.toSlice() sort.Strings(ps) entitiesWithMemberOf.Policies = ps resMap[user] = entitiesWithMemberOf return true }) for _, v := range resMap { if v.Policies != nil || v.MemberOfMappings != nil { r = append(r, v) } } sort.Slice(r, func(i, j int) bool { return r[i].User < r[j].User }) return r } // Assumes store is locked by caller. If groups is empty, returns all group mappings. func (store *IAMStoreSys) listGroupPolicyMappings(cache *iamCache, groupsSet set.StringSet, groupPredicate func(string) bool, decodeFunc func(string) string, ) []madmin.GroupPolicyEntities { var r []madmin.GroupPolicyEntities cache.iamGroupPolicyMap.Range(func(group string, mappedPolicy MappedPolicy) bool { if groupPredicate != nil && !groupPredicate(group) { return true } if !groupsSet.IsEmpty() && !groupsSet.Contains(group) { return true } decodeGroup := group if decodeFunc != nil { decodeGroup = decodeFunc(group) } ps := mappedPolicy.toSlice() sort.Strings(ps) r = append(r, madmin.GroupPolicyEntities{ Group: decodeGroup, Policies: ps, }) return true }) sort.Slice(r, func(i, j int) bool { return r[i].Group < r[j].Group }) return r } // Assumes store is locked by caller. If policies is empty, returns all policy mappings. func (store *IAMStoreSys) listPolicyMappings(cache *iamCache, queryPolSet set.StringSet, userPredicate, groupPredicate func(string) bool, decodeFunc func(string) string, ) []madmin.PolicyEntities { policyToUsersMap := make(map[string]set.StringSet) cache.iamUserPolicyMap.Range(func(user string, mappedPolicy MappedPolicy) bool { if userPredicate != nil && !userPredicate(user) { return true } decodeUser := user if decodeFunc != nil { decodeUser = decodeFunc(user) } commonPolicySet := mappedPolicy.policySet() if !queryPolSet.IsEmpty() { commonPolicySet = commonPolicySet.Intersection(queryPolSet) } for _, policy := range commonPolicySet.ToSlice() { s, ok := policyToUsersMap[policy] if !ok { policyToUsersMap[policy] = set.CreateStringSet(decodeUser) } else { s.Add(decodeUser) policyToUsersMap[policy] = s } } return true }) if iamOS, ok := store.IAMStorageAPI.(*IAMObjectStore); ok { for item := range listIAMConfigItems(context.Background(), iamOS.objAPI, iamConfigPrefix+SlashSeparator+policyDBSTSUsersListKey) { user := strings.TrimSuffix(item.Item, ".json") if userPredicate != nil && !userPredicate(user) { continue } decodeUser := user if decodeFunc != nil { decodeUser = decodeFunc(user) } var mappedPolicy MappedPolicy store.loadIAMConfig(context.Background(), &mappedPolicy, getMappedPolicyPath(user, stsUser, false)) commonPolicySet := mappedPolicy.policySet() if !queryPolSet.IsEmpty() { commonPolicySet = commonPolicySet.Intersection(queryPolSet) } for _, policy := range commonPolicySet.ToSlice() { s, ok := policyToUsersMap[policy] if !ok { policyToUsersMap[policy] = set.CreateStringSet(decodeUser) } else { s.Add(decodeUser) policyToUsersMap[policy] = s } } } } if iamOS, ok := store.IAMStorageAPI.(*IAMEtcdStore); ok { m := xsync.NewMapOf[string, MappedPolicy]() err := iamOS.loadMappedPolicies(context.Background(), stsUser, false, m) if err == nil { m.Range(func(user string, mappedPolicy MappedPolicy) bool { if userPredicate != nil && !userPredicate(user) { return true } decodeUser := user if decodeFunc != nil { decodeUser = decodeFunc(user) } commonPolicySet := mappedPolicy.policySet() if !queryPolSet.IsEmpty() { commonPolicySet = commonPolicySet.Intersection(queryPolSet) } for _, policy := range commonPolicySet.ToSlice() { s, ok := policyToUsersMap[policy] if !ok { policyToUsersMap[policy] = set.CreateStringSet(decodeUser) } else { s.Add(decodeUser) policyToUsersMap[policy] = s } } return true }) } } policyToGroupsMap := make(map[string]set.StringSet) cache.iamGroupPolicyMap.Range(func(group string, mappedPolicy MappedPolicy) bool { if groupPredicate != nil && !groupPredicate(group) { return true } decodeGroup := group if decodeFunc != nil { decodeGroup = decodeFunc(group) } commonPolicySet := mappedPolicy.policySet() if !queryPolSet.IsEmpty() { commonPolicySet = commonPolicySet.Intersection(queryPolSet) } for _, policy := range commonPolicySet.ToSlice() { s, ok := policyToGroupsMap[policy] if !ok { policyToGroupsMap[policy] = set.CreateStringSet(decodeGroup) } else { s.Add(decodeGroup) policyToGroupsMap[policy] = s } } return true }) m := make(map[string]madmin.PolicyEntities, len(policyToGroupsMap)) for policy, groups := range policyToGroupsMap { s := groups.ToSlice() sort.Strings(s) m[policy] = madmin.PolicyEntities{ Policy: policy, Groups: s, } } for policy, users := range policyToUsersMap { s := users.ToSlice() sort.Strings(s) // Update existing value in map pe := m[policy] pe.Policy = policy pe.Users = s m[policy] = pe } policyEntities := make([]madmin.PolicyEntities, 0, len(m)) for _, v := range m { policyEntities = append(policyEntities, v) } sort.Slice(policyEntities, func(i, j int) bool { return policyEntities[i].Policy < policyEntities[j].Policy }) return policyEntities } // ListPolicyMappings - return users/groups mapped to policies. func (store *IAMStoreSys) ListPolicyMappings(q cleanEntitiesQuery, userPredicate, groupPredicate func(string) bool, decodeFunc func(string) string, ) madmin.PolicyEntitiesResult { cache := store.rlock() defer store.runlock() var result madmin.PolicyEntitiesResult isAllPoliciesQuery := len(q.Users) == 0 && len(q.Groups) == 0 && len(q.Policies) == 0 if len(q.Users) > 0 { result.UserMappings = store.listUserPolicyMappings(cache, q.Users, userPredicate, decodeFunc) } if len(q.Groups) > 0 { result.GroupMappings = store.listGroupPolicyMappings(cache, q.Groups, groupPredicate, decodeFunc) } if len(q.Policies) > 0 || isAllPoliciesQuery { result.PolicyMappings = store.listPolicyMappings(cache, q.Policies, userPredicate, groupPredicate, decodeFunc) } return result } // SetUserStatus - sets current user status. func (store *IAMStoreSys) SetUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) (updatedAt time.Time, err error) { if accessKey != "" && status != madmin.AccountEnabled && status != madmin.AccountDisabled { return updatedAt, errInvalidArgument } cache := store.lock() defer store.unlock() ui, ok := cache.iamUsersMap[accessKey] if !ok { return updatedAt, errNoSuchUser } cred := ui.Credentials if cred.IsTemp() || cred.IsServiceAccount() { return updatedAt, errIAMActionNotAllowed } uinfo := newUserIdentity(auth.Credentials{ AccessKey: accessKey, SecretKey: cred.SecretKey, Status: func() string { switch string(status) { case string(madmin.AccountEnabled), string(auth.AccountOn): return auth.AccountOn } return auth.AccountOff }(), }) if err := store.saveUserIdentity(ctx, accessKey, regUser, &uinfo); err != nil { return updatedAt, err } if err := cache.updateUserWithClaims(accessKey, uinfo); err != nil { return updatedAt, err } return uinfo.UpdatedAt, nil } // AddServiceAccount - add a new service account func (store *IAMStoreSys) AddServiceAccount(ctx context.Context, cred auth.Credentials) (updatedAt time.Time, err error) { cache := store.lock() defer store.unlock() accessKey := cred.AccessKey parentUser := cred.ParentUser // Local creates cannot replace an existing key. Replication also sends // full snapshots after deletion/recreation, possibly with a new owner; // their source revision is compared atomically by saveUserIdentity. _, replicated := iamReplicationTime(ctx) if su, found := cache.iamUsersMap[accessKey]; found { scred := su.Credentials if !replicated || !scred.IsServiceAccount() { if scred.ParentUser != parentUser { return updatedAt, fmt.Errorf("%w: the service account access key is taken by another user", errIAMServiceAccountNotAllowed) } return updatedAt, fmt.Errorf("%w: the service account access key already taken", errIAMServiceAccountNotAllowed) } } if _, found := cache.iamSTSAccountsMap[accessKey]; found { return updatedAt, fmt.Errorf("%w: the service account access key is taken by a temporary credential", errIAMServiceAccountNotAllowed) } // Parent user must not be a service account. if u, found := cache.iamUsersMap[parentUser]; found && u.Credentials.IsServiceAccount() { return updatedAt, fmt.Errorf("%w: unable to create a service account for another service account", errIAMServiceAccountNotAllowed) } u := newUserIdentity(cred) err = store.saveUserIdentity(ctx, u.Credentials.AccessKey, svcUser, &u) if err != nil { return updatedAt, err } if err := cache.updateUserWithClaims(u.Credentials.AccessKey, u); err != nil { // Persistence already replaced the credential. A failed refresh must // not leave its superseded secret usable through this node's cache. delete(cache.iamUsersMap, accessKey) cache.updatedAt = UTCNow() return updatedAt, err } return u.UpdatedAt, nil } // UpdateServiceAccount - updates a service account on storage. func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey string, opts updateServiceAccountOpts) (updatedAt time.Time, err error) { cache := store.lock() defer store.unlock() ui, ok := cache.iamUsersMap[accessKey] if !ok || !ui.Credentials.IsServiceAccount() { return updatedAt, errNoSuchServiceAccount } cr := ui.Credentials currentSecretKey := cr.SecretKey if opts.secretKey != "" { if !auth.IsSecretKeyValid(opts.secretKey) { return updatedAt, auth.ErrInvalidSecretKeyLength } cr.SecretKey = opts.secretKey } if opts.name != "" { cr.Name = opts.name } if opts.description != "" { cr.Description = opts.description } if opts.expiration != nil { expirationInUTC := opts.expiration.UTC() if err := validateSvcExpirationInUTC(ctx, expirationInUTC); err != nil { return updatedAt, err } cr.Expiration = expirationInUTC } switch opts.status { // The caller did not ask to update status account, do nothing case "": case string(madmin.AccountEnabled): cr.Status = auth.AccountOn case string(madmin.AccountDisabled): cr.Status = auth.AccountOff // Update account status case auth.AccountOn, auth.AccountOff: cr.Status = opts.status default: return updatedAt, errors.New("unknown account status value") } m, err := getClaimsFromTokenWithSecret(cr.SessionToken, currentSecretKey) if err != nil { return updatedAt, fmt.Errorf("unable to get svc acc claims: %v", err) } // Extracted session policy name string can be removed as its not useful // at this point. m.Delete(sessionPolicyNameExtracted) nosp := opts.sessionPolicy == nil || opts.sessionPolicy.Version == "" && len(opts.sessionPolicy.Statements) == 0 // sessionPolicy is nil and there is embedded policy attached we remove // embedded policy at that point. if _, ok := m.Lookup(policy.SessionPolicyName); ok && nosp { m.Delete(policy.SessionPolicyName) m.Set(iamPolicyClaimNameSA(), inheritedPolicyType) } if opts.sessionPolicy != nil { // session policies is being updated if err := opts.sessionPolicy.Validate(); err != nil { return updatedAt, err } if opts.sessionPolicy.Version != "" && len(opts.sessionPolicy.Statements) > 0 { policyBuf, err := json.Marshal(opts.sessionPolicy) if err != nil { return updatedAt, err } if len(policyBuf) > maxSVCSessionPolicySize { return updatedAt, errSessionPolicyTooLarge } // Overwrite session policy claims. m.Set(policy.SessionPolicyName, base64.StdEncoding.EncodeToString(policyBuf)) m.Set(iamPolicyClaimNameSA(), embeddedPolicyType) } } cr.SessionToken, err = auth.JWTSignWithAccessKey(accessKey, m.Map(), cr.SecretKey) if err != nil { return updatedAt, err } u := newUserIdentity(cr) if err := store.saveUserIdentity(ctx, u.Credentials.AccessKey, svcUser, &u); err != nil { return updatedAt, err } if err := cache.updateUserWithClaims(u.Credentials.AccessKey, u); err != nil { delete(cache.iamUsersMap, accessKey) cache.updatedAt = UTCNow() return updatedAt, err } return u.UpdatedAt, nil } // ListTempAccounts - lists only temporary accounts from the cache. func (store *IAMStoreSys) ListTempAccounts(ctx context.Context, accessKey string) ([]UserIdentity, error) { cache := store.rlock() defer store.runlock() userExists := false var tempAccounts []UserIdentity for _, v := range cache.iamUsersMap { isDerived := false if v.Credentials.IsServiceAccount() || v.Credentials.IsTemp() { isDerived = true } if !isDerived && v.Credentials.AccessKey == accessKey { userExists = true } else if isDerived && v.Credentials.ParentUser == accessKey { userExists = true if v.Credentials.IsTemp() { // Hide secret key & session key here v.Credentials.SecretKey = "" v.Credentials.SessionToken = "" tempAccounts = append(tempAccounts, v) } } } if !userExists { return nil, errNoSuchUser } return tempAccounts, 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 _, u := range cache.iamUsersMap { v := u.Credentials if accessKey != "" && v.ParentUser == accessKey { if v.IsServiceAccount() { // Hide secret key & session key here v.SecretKey = "" v.SessionToken = "" serviceAccounts = append(serviceAccounts, v) } } } return serviceAccounts, nil } // ListSTSAccounts - lists only STS accounts from the cache. func (store *IAMStoreSys) ListSTSAccounts(ctx context.Context, accessKey string) ([]auth.Credentials, error) { cache := store.rlock() defer store.runlock() var stsAccounts []auth.Credentials for _, u := range cache.iamSTSAccountsMap { v := u.Credentials if accessKey != "" && v.ParentUser == accessKey { if v.IsTemp() { // Hide secret key & session key here v.SecretKey = "" v.SessionToken = "" stsAccounts = append(stsAccounts, v) } } } return stsAccounts, nil } // ListAccessKeys - lists all access keys (sts/service accounts) func (store *IAMStoreSys) ListAccessKeys(ctx context.Context) ([]auth.Credentials, error) { cache := store.rlock() defer store.runlock() accessKeys := store.getSTSAndServiceAccounts(cache) for i, accessKey := range accessKeys { accessKeys[i].SecretKey = "" if accessKey.IsTemp() { secret, err := getTokenSigningKey() if err != nil { return nil, err } claims, err := getClaimsFromTokenWithSecret(accessKey.SessionToken, secret) if err != nil { continue // ignore invalid session tokens } accessKeys[i].Claims = claims.MapClaims } accessKeys[i].SessionToken = "" } return accessKeys, nil } // AddUser - adds/updates long term user account to storage. func (store *IAMStoreSys) AddUser(ctx context.Context, accessKey string, ureq madmin.AddOrUpdateUserReq) (updatedAt time.Time, err error) { cache := store.lock() defer store.unlock() cache.updatedAt = time.Now() ui, ok := cache.iamUsersMap[accessKey] // It is not possible to update an STS account. if ok && ui.Credentials.IsTemp() { return updatedAt, errIAMActionNotAllowed } u := newUserIdentity(auth.Credentials{ AccessKey: accessKey, SecretKey: ureq.SecretKey, Status: func() string { switch string(ureq.Status) { case string(madmin.AccountEnabled), string(auth.AccountOn): return auth.AccountOn } return auth.AccountOff }(), }) if err := store.saveUserIdentity(ctx, accessKey, regUser, &u); err != nil { return updatedAt, err } if err := cache.updateUserWithClaims(accessKey, u); err != nil { return updatedAt, err } return u.UpdatedAt, 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() cache.updatedAt = time.Now() ui, ok := cache.iamUsersMap[accessKey] if !ok { return errNoSuchUser } cred := ui.Credentials cred.SecretKey = secretKey u := newUserIdentity(cred) if err := store.saveUserIdentity(ctx, accessKey, regUser, &u); err != nil { return err } return cache.updateUserWithClaims(accessKey, u) } // GetSTSAndServiceAccounts - returns all STS and Service account credentials. func (store *IAMStoreSys) GetSTSAndServiceAccounts() []auth.Credentials { cache := store.rlock() defer store.runlock() return store.getSTSAndServiceAccounts(cache) } func (store *IAMStoreSys) getSTSAndServiceAccounts(cache *iamCache) []auth.Credentials { var res []auth.Credentials for _, u := range cache.iamUsersMap { cred := u.Credentials if cred.IsServiceAccount() { res = append(res, cred) } } for _, u := range cache.iamSTSAccountsMap { res = append(res, u.Credentials) } return res } // UpdateUserIdentity - updates a user credential. func (store *IAMStoreSys) UpdateUserIdentity(ctx context.Context, cred auth.Credentials) error { cache := store.lock() defer store.unlock() cache.updatedAt = time.Now() userType := regUser if cred.IsServiceAccount() { userType = svcUser } else if cred.IsTemp() { userType = stsUser } ui := newUserIdentity(cred) // Overwrite the user identity here. As store should be // atomic, it shouldn't cause any corruption. if err := store.saveUserIdentity(ctx, cred.AccessKey, userType, &ui); err != nil { return err } return cache.updateUserWithClaims(cred.AccessKey, ui) } // LoadUser - attempts to load user info from storage and updates cache. func (store *IAMStoreSys) LoadUser(ctx context.Context, accessKey string) error { groupLoad := env.Get("_MINIO_IAM_GROUP_REFRESH", config.EnableOff) == config.EnableOn newCachePopulate := func() (val any, err error) { newCache := newIamCache() newCache.revisions = store.revisionIndex() // Check for service account first store.loadUser(ctx, accessKey, svcUser, newCache.iamUsersMap) svc, found := newCache.iamUsersMap[accessKey] if found { // Load parent user and mapped policies. if store.getUsersSysType() == MinIOUsersSysType { err = store.loadUser(ctx, svc.Credentials.ParentUser, regUser, newCache.iamUsersMap) // NOTE: we are not worried about loading errors from policies. store.loadMappedPolicyWithRetry(ctx, svc.Credentials.ParentUser, regUser, false, newCache.iamUserPolicyMap, 3) } else { // In case of LDAP the parent user's policy mapping needs to be loaded into sts map // NOTE: we are not worried about loading errors from policies. store.loadMappedPolicyWithRetry(ctx, svc.Credentials.ParentUser, stsUser, false, newCache.iamSTSPolicyMap, 3) } } if !found { err = store.loadUser(ctx, accessKey, regUser, newCache.iamUsersMap) if _, found = newCache.iamUsersMap[accessKey]; found { // NOTE: we are not worried about loading errors from policies. store.loadMappedPolicyWithRetry(ctx, accessKey, regUser, false, newCache.iamUserPolicyMap, 3) } } // Check for STS account var stsUserCred UserIdentity if !found { err = store.loadUser(ctx, accessKey, stsUser, newCache.iamSTSAccountsMap) if stsUserCred, found = newCache.iamSTSAccountsMap[accessKey]; found { // Load mapped policy // NOTE: we are not worried about loading errors from policies. store.loadMappedPolicyWithRetry(ctx, stsUserCred.Credentials.ParentUser, stsUser, false, newCache.iamSTSPolicyMap, 3) } } // Load any associated policy definitions pols, _ := newCache.iamUserPolicyMap.Load(accessKey) for _, policy := range pols.toSlice() { if _, found = newCache.iamPolicyDocsMap[policy]; !found { // NOTE: we are not worried about loading errors from policies. store.loadPolicyDocWithRetry(ctx, policy, newCache.iamPolicyDocsMap, 3) } } pols, _ = newCache.iamSTSPolicyMap.Load(stsUserCred.Credentials.AccessKey) for _, policy := range pols.toSlice() { if _, found = newCache.iamPolicyDocsMap[policy]; !found { // NOTE: we are not worried about loading errors from policies. store.loadPolicyDocWithRetry(ctx, policy, newCache.iamPolicyDocsMap, 3) } } if groupLoad { // NOTE: we are not worried about loading errors from groups. store.updateGroups(ctx, newCache) newCache.buildUserGroupMemberships() } return newCache, err } var ( val any err error ) if store.group != nil { val, err, _ = store.group.Do(accessKey, newCachePopulate) } else { val, err = newCachePopulate() } // Return error right away if any. if err != nil { if errors.Is(err, errNoSuchUser) || errors.Is(err, errConfigNotFound) { return nil } return err } newCache, ok := val.(*iamCache) if !ok { return nil } cache := store.lock() defer store.unlock() // We need to merge the new cache with the existing cache because the // periodic IAM reload is partial. The periodic load here is to account. newCache.iamGroupPolicyMap.Range(func(k string, v MappedPolicy) bool { cache.iamGroupPolicyMap.Store(k, v) return true }) maps.Copy(cache.iamGroupsMap, newCache.iamGroupsMap) maps.Copy(cache.iamPolicyDocsMap, newCache.iamPolicyDocsMap) maps.Copy(cache.iamUserGroupMemberships, newCache.iamUserGroupMemberships) newCache.iamUserPolicyMap.Range(func(k string, v MappedPolicy) bool { cache.iamUserPolicyMap.Store(k, v) return true }) maps.Copy(cache.iamUsersMap, newCache.iamUsersMap) maps.Copy(cache.iamSTSAccountsMap, newCache.iamSTSAccountsMap) newCache.iamSTSPolicyMap.Range(func(k string, v MappedPolicy) bool { cache.iamSTSPolicyMap.Store(k, v) return true }) cache.updatedAt = time.Now() return nil } func extractJWTClaims(u UserIdentity) (jwtClaims *jwt.MapClaims, err error) { keys := make([]string, 0, 3) // Append credentials secret key itself keys = append(keys, u.Credentials.SecretKey) // Use site-replication credentials if found if globalSiteReplicationSys.isEnabled() { secretKey, err := getTokenSigningKey() if err != nil { return nil, err } keys = append(keys, secretKey) } // Iterate over all keys and return with the first successful claim extraction for _, key := range keys { jwtClaims, err = getClaimsFromTokenWithSecret(u.Credentials.SessionToken, key) if err == nil { break } } return jwtClaims, err } func validateSvcExpirationInUTC(ctx context.Context, expirationInUTC time.Time) error { if _, replicated := iamReplicationTime(ctx); replicated { // Issuance limits were checked at the source. Keep its absolute // expiration, even if it has passed: persisting the newer expired // identity denies the older credential and lets ordinary cleanup // retain a tombstone. Rejecting it could leave an old key alive. return nil } if expirationInUTC.IsZero() || expirationInUTC.Equal(timeSentinel) { // Service accounts might not have expiration in older releases. return nil } currentTime := time.Now().UTC() minExpiration := currentTime.Add(minServiceAccountExpiry) maxExpiration := currentTime.Add(maxServiceAccountExpiry) if expirationInUTC.Before(minExpiration) || expirationInUTC.After(maxExpiration) { return errInvalidSvcAcctExpiration } return nil }