minio/cmd/iam.go

2528 lines
66 KiB
Go
Raw Normal View History

// 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 <http://www.gnu.org/licenses/>.
package cmd
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math/rand"
"strings"
"sync"
"time"
humanize "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"
)
// UsersSysType - defines the type of users and groups system that is
// active on the server.
type UsersSysType string
// Types of users configured in the server.
const (
// This mode uses the internal users system in MinIO.
MinIOUsersSysType UsersSysType = "MinIOUsersSys"
// This mode uses users and groups from a configured LDAP
// server.
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
usersSysType UsersSysType
// map of policy names to policy definitions
iamPolicyDocsMap map[string]iampolicy.Policy
// map of usernames to credentials
iamUsersMap map[string]auth.Credentials
// map of group names to group info
iamGroupsMap map[string]GroupInfo
// map of user names to groups they are a member of
iamUserGroupMemberships map[string]set.StringSet
// map of usernames/temporary access keys to policy names
iamUserPolicyMap map[string]MappedPolicy
// map of group names to policy names
iamGroupPolicyMap map[string]MappedPolicy
// Persistence layer for IAM subsystem
store IAMStorageAPI
// configLoaded will be closed and remain so after first load.
configLoaded chan struct{}
}
// IAMUserType represents a user type inside MinIO server
type IAMUserType int
const (
regUser IAMUserType = iota
stsUser
svcUser
)
// key options
type options struct {
ttl int64 //expiry in seconds
}
// 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
loadAll(context.Context, *IAMSys) 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
watch(context.Context, *IAMSys)
}
// 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
// simplifies the implementation for group removal. This is called
// only via IAM notifications.
func (sys *IAMSys) LoadGroup(objAPI ObjectLayer, group string) error {
if !sys.Initialized() {
return errServerNotInitialized
}
if globalEtcdClient != nil {
// Watch APIs cover this case, so nothing to do.
return nil
}
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.
func (sys *IAMSys) LoadPolicy(objAPI ObjectLayer, policyName string) error {
if !sys.Initialized() {
return errServerNotInitialized
}
sys.store.lock()
defer sys.store.unlock()
if globalEtcdClient == nil {
return sys.store.loadPolicyDoc(context.Background(), policyName, sys.iamPolicyDocsMap)
}
// When etcd is set, we use watch APIs so this code is not needed.
return nil
}
// LoadPolicyMapping - loads the mapped policy for a user or group
// from storage into server memory.
func (sys *IAMSys) LoadPolicyMapping(objAPI ObjectLayer, userOrGroup string, isGroup bool) error {
if !sys.Initialized() {
return errServerNotInitialized
}
sys.store.lock()
defer sys.store.unlock()
if globalEtcdClient == nil {
var err error
userType := regUser
if sys.usersSysType == LDAPUsersSysType {
userType = stsUser
}
if isGroup {
err = sys.store.loadMappedPolicy(context.Background(), userOrGroup, userType, isGroup, sys.iamGroupPolicyMap)
} else {
err = sys.store.loadMappedPolicy(context.Background(), userOrGroup, userType, isGroup, sys.iamUserPolicyMap)
}
if err == errNoSuchPolicy {
if isGroup {
delete(sys.iamGroupPolicyMap, userOrGroup)
} else {
delete(sys.iamUserPolicyMap, userOrGroup)
}
}
// Ignore policy not mapped error
if err != nil && err != errNoSuchPolicy {
return err
}
}
// When etcd is set, we use watch APIs so this code is not needed.
return nil
}
// LoadUser - reloads a specific user from backend disks or etcd.
func (sys *IAMSys) LoadUser(objAPI ObjectLayer, accessKey string, userType IAMUserType) error {
if !sys.Initialized() {
return errServerNotInitialized
}
sys.store.lock()
defer sys.store.unlock()
if globalEtcdClient == nil {
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 != 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 := 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]
}
}
}
}
// When etcd is set, we use watch APIs so this code is not needed.
return nil
}
// LoadServiceAccount - reloads a specific service account from backend disks or etcd.
func (sys *IAMSys) LoadServiceAccount(accessKey string) error {
if !sys.Initialized() {
return errServerNotInitialized
}
sys.store.lock()
defer sys.store.unlock()
if globalEtcdClient == nil {
err := sys.store.loadUser(context.Background(), accessKey, svcUser, sys.iamUsersMap)
if err != nil {
return err
}
}
// When etcd is set, we use watch APIs so this code is not needed.
return nil
}
// Perform IAM configuration migration.
func (sys *IAMSys) doIAMConfigMigration(ctx context.Context) error {
return sys.store.migrateBackendFormat(ctx)
}
// InitStore initializes IAM stores
func (sys *IAMSys) InitStore(objAPI ObjectLayer) {
sys.Lock()
defer sys.Unlock()
if globalEtcdClient == nil {
if globalIsGateway {
sys.store = &iamDummyStore{}
} else {
sys.store = newIAMObjectStore(objAPI)
}
} else {
sys.store = newIAMEtcdStore()
}
if globalLDAPConfig.Enabled {
sys.EnableLDAPSys()
}
}
// Initialized check if IAM is initialized
func (sys *IAMSys) Initialized() bool {
if sys == nil {
return false
}
sys.Lock()
defer sys.Unlock()
return sys.store != nil
}
// Load - loads all credentials
func (sys *IAMSys) Load(ctx context.Context, store IAMStorageAPI) error {
iamUsersMap := make(map[string]auth.Credentials)
iamGroupsMap := make(map[string]GroupInfo)
iamUserPolicyMap := make(map[string]MappedPolicy)
iamGroupPolicyMap := make(map[string]MappedPolicy)
iamPolicyDocsMap := make(map[string]iampolicy.Policy)
store.rlock()
isMinIOUsersSys := sys.usersSysType == MinIOUsersSysType
store.runlock()
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
}
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
}
store.lock()
defer store.unlock()
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:
close(sys.configLoaded)
}
return nil
}
// Init - initializes config system by reading entries from config/iam
func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer) {
// Initialize IAM store
sys.InitStore(objAPI)
retryCtx, cancel := context.WithCancel(ctx)
// Indicate to our routine to exit cleanly upon return.
defer cancel()
// Hold the lock for migration only.
txnLk := objAPI.NewNSLock(minioMetaBucket, minioConfigPrefix+"/iam.lock")
// allocate dynamic timeout once before the loop
iamLockTimeout := newDynamicTimeout(5*time.Second, 3*time.Second)
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for {
// let one of the server acquire the lock, if not let them timeout.
// which shall be retried again by this loop.
lkctx, err := txnLk.GetLock(retryCtx, iamLockTimeout)
if err != nil {
logger.Info("Waiting for all MinIO IAM sub-system to be initialized.. trying to acquire lock")
time.Sleep(time.Duration(r.Float64() * float64(5*time.Second)))
continue
}
if globalEtcdClient != nil {
// **** WARNING ****
// Migrating to encrypted backend on etcd should happen before initialization of
// IAM sub-system, make sure that we do not move the above codeblock elsewhere.
if err := migrateIAMConfigsEtcdToEncrypted(retryCtx, globalEtcdClient); err != nil {
txnLk.Unlock(lkctx.Cancel)
logger.LogIf(ctx, fmt.Errorf("Unable to decrypt an encrypted ETCD backend for IAM users and policies: %w", err))
logger.LogIf(ctx, errors.New("IAM sub-system is partially initialized, some users may not be available"))
return
}
}
// These messages only meant primarily for distributed setup, so only log during distributed setup.
if globalIsDistErasure {
logger.Info("Waiting for all MinIO IAM sub-system to be initialized.. lock acquired")
}
// Migrate IAM configuration, if necessary.
if err := sys.doIAMConfigMigration(retryCtx); err != nil {
txnLk.Unlock(lkctx.Cancel)
if configRetriableErrors(err) {
logger.Info("Waiting for all MinIO IAM sub-system to be initialized.. possible cause (%v)", err)
continue
}
logger.LogIf(ctx, fmt.Errorf("Unable to migrate IAM users and policies to new format: %w", err))
logger.LogIf(ctx, errors.New("IAM sub-system is partially initialized, some users may not be available"))
return
}
// Successfully migrated, proceed to load the users.
txnLk.Unlock(lkctx.Cancel)
break
}
for {
if err := sys.store.loadAll(retryCtx, sys); err != nil {
if configRetriableErrors(err) {
logger.Info("Waiting for all MinIO IAM sub-system to be initialized.. possible cause (%v)", err)
time.Sleep(time.Duration(r.Float64() * float64(5*time.Second)))
continue
}
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize IAM sub-system, some users may not be available %w", err))
}
}
break
}
// Set up polling for expired accounts and credentials purging.
switch {
case globalOpenIDConfig.ProviderEnabled():
go func() {
for {
time.Sleep(globalRefreshIAMInterval)
sys.purgeExpiredCredentialsForExternalSSO(ctx)
}
}()
case globalLDAPConfig.EnabledWithLookupBind():
go func() {
for {
time.Sleep(globalRefreshIAMInterval)
sys.purgeExpiredCredentialsForLDAP(ctx)
}
}()
}
go sys.store.watch(ctx, sys)
}
// DeletePolicy - deletes a canned policy from backend or etcd.
func (sys *IAMSys) DeletePolicy(policyName string) error {
if !sys.Initialized() {
return errServerNotInitialized
}
if policyName == "" {
return errInvalidArgument
}
sys.store.lock()
defer sys.store.unlock()
err := sys.store.deletePolicyDoc(context.Background(), policyName)
if err == errNoSuchPolicy {
// Ignore error if policy is already deleted.
err = nil
}
delete(sys.iamPolicyDocsMap, policyName)
// Delete user-policy mappings that will no longer apply
for u, mp := range sys.iamUserPolicyMap {
pset := mp.policySet()
if pset.Contains(policyName) {
cr, ok := sys.iamUsersMap[u]
if !ok {
// This case can happen when an temporary account
// is deleted or expired, removed it from userPolicyMap.
delete(sys.iamUserPolicyMap, u)
continue
}
pset.Remove(policyName)
// User is from STS if the cred are temporary
if cr.IsTemp() {
sys.policyDBSet(u, strings.Join(pset.ToSlice(), ","), stsUser, false)
} else {
sys.policyDBSet(u, strings.Join(pset.ToSlice(), ","), regUser, false)
}
}
}
// Delete group-policy mappings that will no longer apply
for g, mp := range sys.iamGroupPolicyMap {
pset := mp.policySet()
if pset.Contains(policyName) {
pset.Remove(policyName)
sys.policyDBSet(g, strings.Join(pset.ToSlice(), ","), regUser, true)
}
}
return err
}
// InfoPolicy - expands the canned policy into its JSON structure.
func (sys *IAMSys) InfoPolicy(policyName string) (iampolicy.Policy, error) {
if !sys.Initialized() {
return iampolicy.Policy{}, errServerNotInitialized
}
sys.store.rlock()
defer sys.store.runlock()
var combinedPolicy iampolicy.Policy
for _, policy := range strings.Split(policyName, ",") {
if policy == "" {
continue
}
v, ok := sys.iamPolicyDocsMap[policy]
if !ok {
return iampolicy.Policy{}, errNoSuchPolicy
}
combinedPolicy = combinedPolicy.Merge(v)
}
return combinedPolicy, nil
}
// ListPolicies - lists all canned policies.
func (sys *IAMSys) ListPolicies(bucketName string) (map[string]iampolicy.Policy, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
<-sys.configLoaded
sys.store.rlock()
defer sys.store.runlock()
policyDocsMap := make(map[string]iampolicy.Policy, len(sys.iamPolicyDocsMap))
for k, v := range sys.iamPolicyDocsMap {
if bucketName != "" && v.MatchResource(bucketName) {
policyDocsMap[k] = v
} else {
policyDocsMap[k] = v
}
}
return policyDocsMap, nil
}
// SetPolicy - sets a new name policy.
func (sys *IAMSys) SetPolicy(policyName string, p iampolicy.Policy) error {
if !sys.Initialized() {
return errServerNotInitialized
}
if p.IsEmpty() || policyName == "" {
return errInvalidArgument
}
sys.store.lock()
defer sys.store.unlock()
if err := sys.store.savePolicyDoc(context.Background(), policyName, p); err != nil {
return err
}
sys.iamPolicyDocsMap[policyName] = p
return nil
}
// DeleteUser - delete user (only for long-term users not STS users).
func (sys *IAMSys) DeleteUser(accessKey string) error {
if !sys.Initialized() {
return errServerNotInitialized
}
if sys.usersSysType != MinIOUsersSysType {
return errIAMActionNotAllowed
}
// First we remove the user from their groups.
userInfo, getErr := sys.GetUserInfo(accessKey)
if getErr != nil {
return getErr
}
for _, group := range userInfo.MemberOf {
removeErr := sys.RemoveUsersFromGroup(group, []string{accessKey})
if removeErr != nil {
return removeErr
}
}
// Next we can remove the user from memory and IAM store
sys.store.lock()
defer sys.store.unlock()
for _, u := range sys.iamUsersMap {
// Delete any service accounts if any first.
if u.IsServiceAccount() {
if u.ParentUser == accessKey {
_ = sys.store.deleteUserIdentity(context.Background(), u.AccessKey, svcUser)
delete(sys.iamUsersMap, u.AccessKey)
}
}
// Delete any associated STS users.
if u.IsTemp() {
if u.ParentUser == accessKey {
_ = sys.store.deleteUserIdentity(context.Background(), u.AccessKey, stsUser)
delete(sys.iamUsersMap, u.AccessKey)
}
}
}
// It is ok to ignore deletion error on the mapped policy
sys.store.deleteMappedPolicy(context.Background(), accessKey, regUser, false)
err := sys.store.deleteUserIdentity(context.Background(), accessKey, regUser)
if err == errNoSuchUser {
// ignore if user is already deleted.
err = nil
}
delete(sys.iamUsersMap, accessKey)
delete(sys.iamUserPolicyMap, accessKey)
return err
}
// CurrentPolicies - returns comma separated policy string, from
// an input policy after validating if there are any current
// policies which exist on MinIO corresponding to the input.
func (sys *IAMSys) CurrentPolicies(policyName string) string {
if !sys.Initialized() {
return ""
}
sys.store.rlock()
defer sys.store.runlock()
var policies []string
mp := newMappedPolicy(policyName)
for _, policy := range mp.toSlice() {
_, found := sys.iamPolicyDocsMap[policy]
if found {
policies = append(policies, policy)
}
}
return strings.Join(policies, ",")
}
// SetTempUser - set temporary user credentials, these credentials have an expiry.
func (sys *IAMSys) SetTempUser(accessKey string, cred auth.Credentials, policyName string) error {
if !sys.Initialized() {
return errServerNotInitialized
}
ttl := int64(cred.Expiration.Sub(UTCNow()).Seconds())
// If OPA is not set we honor any policy claims for this
// temporary user which match with pre-configured canned
// policies for this server.
if globalPolicyOPA == nil && policyName != "" {
mp := newMappedPolicy(policyName)
combinedPolicy := sys.GetCombinedPolicy(mp.toSlice()...)
if combinedPolicy.IsEmpty() {
return fmt.Errorf("specified policy %s, not found %w", policyName, errNoSuchPolicy)
}
sys.store.lock()
defer sys.store.unlock()
if err := sys.store.saveMappedPolicy(context.Background(), accessKey, stsUser, false, mp, options{ttl: ttl}); err != nil {
return err
}
sys.iamUserPolicyMap[accessKey] = mp
// We are on purpose not persisting the policy map for parent
// user, although this is a hack, it is a good enough hack
// at this point in time - we need to overhaul our OIDC
// usage with service accounts with a more cleaner implementation
//
// This mapping is necessary to ensure that valid credentials
// have necessary ParentUser present - this is mainly for only
// webIdentity based STS tokens.
if cred.IsTemp() && cred.ParentUser != "" && cred.ParentUser != globalActiveCred.AccessKey {
if _, ok := sys.iamUserPolicyMap[cred.ParentUser]; !ok {
sys.iamUserPolicyMap[cred.ParentUser] = mp
}
}
} else {
sys.store.lock()
defer sys.store.unlock()
}
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 bucket == "" {
return nil, errInvalidArgument
}
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() {
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.
func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
<-sys.configLoaded
sys.store.rlock()
defer sys.store.runlock()
var users = make(map[string]madmin.UserInfo)
for k, v := range sys.iamUsersMap {
if !v.IsTemp() && !v.IsServiceAccount() {
users[k] = madmin.UserInfo{
PolicyName: sys.iamUserPolicyMap[k].Policies,
Status: func() madmin.AccountStatus {
if v.IsValid() {
return madmin.AccountEnabled
}
return madmin.AccountDisabled
}(),
MemberOf: sys.iamUserGroupMemberships[k].ToSlice(),
}
}
}
if sys.usersSysType == LDAPUsersSysType {
for k, v := range sys.iamUserPolicyMap {
users[k] = madmin.UserInfo{
PolicyName: v.Policies,
Status: madmin.AccountEnabled,
}
}
}
return users, nil
}
// IsTempUser - returns if given key is a temporary user.
func (sys *IAMSys) IsTempUser(name string) (bool, string, error) {
if !sys.Initialized() {
return false, "", errServerNotInitialized
}
sys.store.rlock()
defer sys.store.runlock()
cred, found := sys.iamUsersMap[name]
if !found {
return false, "", errNoSuchUser
}
if cred.IsTemp() {
return true, cred.ParentUser, nil
}
return false, "", nil
}
// IsServiceAccount - returns if given key is a service account
func (sys *IAMSys) IsServiceAccount(name string) (bool, string, error) {
if !sys.Initialized() {
return false, "", errServerNotInitialized
}
sys.store.rlock()
defer sys.store.runlock()
cred, found := sys.iamUsersMap[name]
if !found {
return false, "", errNoSuchUser
}
if cred.IsServiceAccount() {
return true, cred.ParentUser, nil
}
return false, "", nil
}
// GetUserInfo - get info on a user.
func (sys *IAMSys) GetUserInfo(name string) (u madmin.UserInfo, err error) {
if !sys.Initialized() {
return u, errServerNotInitialized
}
select {
case <-sys.configLoaded:
default:
sys.loadUserFromStore(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
}
2018-10-16 17:55:23 -04:00
// SetUserStatus - sets current user status, supports disabled or enabled.
func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus) error {
if !sys.Initialized() {
2018-10-16 17:55:23 -04:00
return errServerNotInitialized
}
if sys.usersSysType != MinIOUsersSysType {
return errIAMActionNotAllowed
}
2018-10-16 17:55:23 -04:00
if status != madmin.AccountEnabled && status != madmin.AccountDisabled {
return errInvalidArgument
}
sys.store.lock()
defer sys.store.unlock()
2018-10-16 17:55:23 -04:00
cred, ok := sys.iamUsersMap[accessKey]
if !ok {
return errNoSuchUser
}
if cred.IsTemp() || cred.IsServiceAccount() {
return errIAMActionNotAllowed
}
uinfo := newUserIdentity(auth.Credentials{
AccessKey: accessKey,
2018-10-16 17:55:23 -04:00
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 {
2018-10-16 17:55:23 -04:00
return err
}
sys.iamUsersMap[accessKey] = uinfo.Credentials
2018-10-16 17:55:23 -04:00
return nil
}
type newServiceAccountOpts struct {
sessionPolicy *iampolicy.Policy
accessKey string
secretKey string
}
// NewServiceAccount - create a new service account
func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, groups []string, opts newServiceAccountOpts) (auth.Credentials, error) {
if !sys.Initialized() {
return auth.Credentials{}, errServerNotInitialized
}
var policyBuf []byte
if opts.sessionPolicy != nil {
err := opts.sessionPolicy.Validate()
if err != nil {
return auth.Credentials{}, err
}
policyBuf, err = json.Marshal(opts.sessionPolicy)
if err != nil {
return auth.Credentials{}, err
}
if len(policyBuf) > 16*humanize.KiByte {
return auth.Credentials{}, fmt.Errorf("Session policy should not exceed 16 KiB characters")
}
}
sys.store.lock()
defer sys.store.unlock()
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
if len(policyBuf) > 0 {
m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf)
m[iamPolicyClaimNameSA()] = "embedded-policy"
} else {
m[iamPolicyClaimNameSA()] = "inherited-policy"
}
var (
cred auth.Credentials
)
if len(opts.accessKey) > 0 {
cred, err = auth.CreateNewCredentialsWithMetadata(opts.accessKey, opts.secretKey, m, globalActiveCred.SecretKey)
} else {
cred, err = auth.GetNewCredentialsWithMetadata(m, globalActiveCred.SecretKey)
}
if err != nil {
return auth.Credentials{}, err
}
cred.ParentUser = parentUser
cred.Groups = groups
cred.Status = string(auth.AccountOn)
u := newUserIdentity(cred)
if err := sys.store.saveUserIdentity(context.Background(), u.Credentials.AccessKey, svcUser, u); err != nil {
return auth.Credentials{}, err
}
sys.iamUsersMap[u.Credentials.AccessKey] = u.Credentials
return cred, nil
}
type updateServiceAccountOpts struct {
sessionPolicy *iampolicy.Policy
secretKey string
status string
}
// UpdateServiceAccount - edit a service account
func (sys *IAMSys) UpdateServiceAccount(ctx context.Context, accessKey string, opts updateServiceAccountOpts) error {
if !sys.Initialized() {
return errServerNotInitialized
}
sys.store.lock()
defer sys.store.unlock()
cr, ok := sys.iamUsersMap[accessKey]
if !ok || !cr.IsServiceAccount() {
return errNoSuchServiceAccount
}
if opts.secretKey != "" {
if !auth.IsSecretKeyValid(opts.secretKey) {
return auth.ErrInvalidSecretKeyLength
}
cr.SecretKey = opts.secretKey
}
switch opts.status {
// The caller did not ask to update status account, do nothing
case "":
// Update account status
case auth.AccountOn, auth.AccountOff:
cr.Status = opts.status
default:
return errors.New("unknown account status value")
}
if opts.sessionPolicy != nil {
m := make(map[string]interface{})
err := opts.sessionPolicy.Validate()
if err != nil {
return err
}
policyBuf, err := json.Marshal(opts.sessionPolicy)
if err != nil {
return err
}
if len(policyBuf) > 16*humanize.KiByte {
return fmt.Errorf("Session policy should not exceed 16 KiB characters")
}
m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf)
m[iamPolicyClaimNameSA()] = "embedded-policy"
m[parentClaim] = cr.ParentUser
cr.SessionToken, err = auth.JWTSignWithAccessKey(accessKey, m, globalActiveCred.SecretKey)
if err != nil {
return err
}
}
u := newUserIdentity(cr)
if err := sys.store.saveUserIdentity(context.Background(), u.Credentials.AccessKey, svcUser, u); err != nil {
return err
}
sys.iamUsersMap[u.Credentials.AccessKey] = u.Credentials
return nil
}
// ListServiceAccounts - lists all services accounts associated to a specific user
func (sys *IAMSys) ListServiceAccounts(ctx context.Context, accessKey string) ([]auth.Credentials, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
<-sys.configLoaded
sys.store.rlock()
defer sys.store.runlock()
var serviceAccounts []auth.Credentials
for _, v := range sys.iamUsersMap {
if v.IsServiceAccount() && v.ParentUser == accessKey {
// Hide secret key & session key here
v.SecretKey = ""
v.SessionToken = ""
serviceAccounts = append(serviceAccounts, v)
}
}
return serviceAccounts, nil
}
// GetServiceAccount - gets information about a service account
func (sys *IAMSys) GetServiceAccount(ctx context.Context, accessKey string) (auth.Credentials, *iampolicy.Policy, error) {
if !sys.Initialized() {
return auth.Credentials{}, nil, errServerNotInitialized
}
sys.store.rlock()
defer sys.store.runlock()
sa, ok := sys.iamUsersMap[accessKey]
if !ok || !sa.IsServiceAccount() {
return auth.Credentials{}, nil, errNoSuchServiceAccount
}
var embeddedPolicy *iampolicy.Policy
jwtClaims, err := auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey)
if err == nil {
pt, ptok := jwtClaims.Lookup(iamPolicyClaimNameSA())
sp, spok := jwtClaims.Lookup(iampolicy.SessionPolicyName)
if ptok && spok && pt == "embedded-policy" {
policyBytes, err := base64.StdEncoding.DecodeString(sp)
if err == nil {
p, err := iampolicy.ParseConfig(bytes.NewReader(policyBytes))
if err == nil {
policy := iampolicy.Policy{}.Merge(*p)
embeddedPolicy = &policy
}
}
}
}
// Hide secret & session keys
sa.SecretKey = ""
sa.SessionToken = ""
return sa, embeddedPolicy, nil
}
// DeleteServiceAccount - delete a service account
func (sys *IAMSys) DeleteServiceAccount(ctx context.Context, accessKey string) error {
if !sys.Initialized() {
return errServerNotInitialized
}
sys.store.lock()
defer sys.store.unlock()
sa, ok := sys.iamUsersMap[accessKey]
if !ok || !sa.IsServiceAccount() {
return nil
}
// It is ok to ignore deletion error on the mapped policy
err := sys.store.deleteUserIdentity(context.Background(), accessKey, svcUser)
if err != nil && err != errNoSuchUser {
return err
}
delete(sys.iamUsersMap, accessKey)
return nil
}
// CreateUser - create new user credentials and policy, if user already exists
// they shall be rewritten with new inputs.
func (sys *IAMSys) CreateUser(accessKey string, uinfo madmin.UserInfo) error {
if !sys.Initialized() {
return errServerNotInitialized
}
if sys.usersSysType != MinIOUsersSysType {
return errIAMActionNotAllowed
}
if !auth.IsAccessKeyValid(accessKey) {
return auth.ErrInvalidAccessKeyLength
}
if !auth.IsSecretKeyValid(uinfo.SecretKey) {
return auth.ErrInvalidSecretKeyLength
}
sys.store.lock()
defer sys.store.unlock()
cr, ok := sys.iamUsersMap[accessKey]
if cr.IsTemp() && ok {
return errIAMActionNotAllowed
}
u := newUserIdentity(auth.Credentials{
AccessKey: accessKey,
SecretKey: uinfo.SecretKey,
Status: func() string {
if uinfo.Status == madmin.AccountEnabled {
return auth.AccountOn
}
return auth.AccountOff
}(),
})
if err := sys.store.saveUserIdentity(context.Background(), accessKey, regUser, u); err != nil {
return err
}
sys.iamUsersMap[accessKey] = u.Credentials
// Set policy if specified.
if uinfo.PolicyName != "" {
return sys.policyDBSet(accessKey, uinfo.PolicyName, regUser, false)
}
return nil
}
// SetUserSecretKey - sets user secret key
func (sys *IAMSys) SetUserSecretKey(accessKey string, secretKey string) error {
if !sys.Initialized() {
return errServerNotInitialized
}
if sys.usersSysType != MinIOUsersSysType {
return errIAMActionNotAllowed
}
if !auth.IsAccessKeyValid(accessKey) {
return auth.ErrInvalidAccessKeyLength
}
if !auth.IsSecretKeyValid(secretKey) {
return auth.ErrInvalidSecretKeyLength
}
sys.store.lock()
defer sys.store.unlock()
cred, ok := sys.iamUsersMap[accessKey]
if !ok {
return errNoSuchUser
}
cred.SecretKey = secretKey
u := newUserIdentity(cred)
if err := sys.store.saveUserIdentity(context.Background(), accessKey, regUser, u); err != nil {
return err
}
sys.iamUsersMap[accessKey] = cred
return nil
}
func (sys *IAMSys) loadUserFromStore(accessKey string) {
sys.store.lock()
// If user is already found proceed.
if _, found := sys.iamUsersMap[accessKey]; !found {
sys.store.loadUser(context.Background(), accessKey, regUser, sys.iamUsersMap)
if _, found = sys.iamUsersMap[accessKey]; found {
// found user, load its mapped policies
sys.store.loadMappedPolicy(context.Background(), accessKey, regUser, false, sys.iamUserPolicyMap)
} else {
sys.store.loadUser(context.Background(), accessKey, svcUser, sys.iamUsersMap)
if svc, found := sys.iamUsersMap[accessKey]; found {
// Found service account, load its parent user and its mapped policies.
if sys.usersSysType == MinIOUsersSysType {
sys.store.loadUser(context.Background(), svc.ParentUser, regUser, sys.iamUsersMap)
}
sys.store.loadMappedPolicy(context.Background(), svc.ParentUser, regUser, false, sys.iamUserPolicyMap)
} else {
// None found fall back to STS users.
sys.store.loadUser(context.Background(), accessKey, stsUser, sys.iamUsersMap)
if _, found = sys.iamUsersMap[accessKey]; found {
// STS user found, load its mapped policy.
sys.store.loadMappedPolicy(context.Background(), accessKey, stsUser, false, sys.iamUserPolicyMap)
}
}
}
}
// Load associated policies if any.
for _, policy := range sys.iamUserPolicyMap[accessKey].toSlice() {
if _, found := sys.iamPolicyDocsMap[policy]; !found {
sys.store.loadPolicyDoc(context.Background(), policy, sys.iamPolicyDocsMap)
}
}
sys.buildUserGroupMemberships()
sys.store.unlock()
}
// purgeExpiredCredentialsForExternalSSO - validates if local credentials are still valid
// by checking remote IDP if the relevant users are still active and present.
func (sys *IAMSys) purgeExpiredCredentialsForExternalSSO(ctx context.Context) {
sys.store.lock()
parentUsersMap := make(map[string][]auth.Credentials, len(sys.iamUsersMap))
for _, cred := range sys.iamUsersMap {
if cred.IsServiceAccount() || cred.IsTemp() {
userid, err := parseOpenIDParentUser(cred.ParentUser)
if err == errSkipFile {
continue
}
parentUsersMap[userid] = append(parentUsersMap[userid], cred)
}
}
sys.store.unlock()
expiredUsers := make([]auth.Credentials, 0, len(parentUsersMap))
for userid, creds := range parentUsersMap {
u, err := globalOpenIDConfig.LookupUser(userid)
if err != nil {
logger.LogIf(GlobalContext, err)
continue
}
// Disabled parentUser purge the entries locally
if !u.Enabled {
expiredUsers = append(expiredUsers, creds...)
}
}
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) {
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)
}
}
}
sys.store.unlock()
expiredUsers, err := globalLDAPConfig.GetNonExistentUserDNS(parentUsers)
if err != nil {
// Log and return on error - perhaps it'll work the next time.
logger.LogIf(GlobalContext, err)
return
}
for _, expiredUser := range expiredUsers {
for _, cred := range parentUsersMap[expiredUser] {
userType := regUser
if cred.IsServiceAccount() {
userType = svcUser
} else if cred.IsTemp() {
userType = stsUser
}
sys.store.deleteIAMConfig(ctx, getUserIdentityPath(cred.AccessKey, userType))
sys.store.deleteIAMConfig(ctx, getMappedPolicyPath(cred.AccessKey, userType, false))
}
}
sys.store.lock()
for _, user := range expiredUsers {
for _, cred := range parentUsersMap[user] {
delete(sys.iamUsersMap, cred.AccessKey)
delete(sys.iamUserPolicyMap, cred.AccessKey)
}
}
sys.store.unlock()
}
// GetUser - get user credentials
func (sys *IAMSys) GetUser(accessKey string) (cred auth.Credentials, ok bool) {
if !sys.Initialized() {
return cred, false
}
fallback := false
select {
case <-sys.configLoaded:
default:
sys.loadUserFromStore(accessKey)
fallback = true
}
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.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.policyDBGet(cred.ParentUser, false)
if err != nil {
// Reject if the policy map for user doesn't exist anymore.
logger.LogIf(context.Background(), fmt.Errorf("'%s' user does not have a policy present", cred.ParentUser))
return auth.Credentials{}, false
}
for _, group := range cred.Groups {
ps, err := sys.policyDBGet(group, true)
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))
return auth.Credentials{}, false
}
policies = append(policies, ps...)
}
ok = len(policies) > 0 || globalPolicyOPA != nil
}
}
return cred, ok && cred.IsValid()
}
// AddUsersToGroup - adds users to a group, creating the group if
// needed. No error if user(s) already are in the group.
func (sys *IAMSys) AddUsersToGroup(group string, members []string) error {
if !sys.Initialized() {
return errServerNotInitialized
}
if group == "" {
return errInvalidArgument
}
if sys.usersSysType != MinIOUsersSysType {
return errIAMActionNotAllowed
}
sys.store.lock()
defer sys.store.unlock()
// Validate that all members exist.
for _, member := range members {
cr, ok := sys.iamUsersMap[member]
if !ok {
return errNoSuchUser
}
if cr.IsTemp() {
return errIAMActionNotAllowed
}
}
gi, ok := sys.iamGroupsMap[group]
if !ok {
// Set group as enabled by default when it doesn't
// exist.
gi = newGroupInfo(members)
} else {
mergedMembers := append(gi.Members, members...)
uniqMembers := set.CreateStringSet(mergedMembers...).ToSlice()
gi.Members = uniqMembers
}
if err := sys.store.saveGroupInfo(context.Background(), group, gi); err != nil {
return err
}
sys.iamGroupsMap[group] = gi
// update user-group membership map
for _, member := range members {
gset := sys.iamUserGroupMemberships[member]
if gset == nil {
gset = set.CreateStringSet(group)
} else {
gset.Add(group)
}
sys.iamUserGroupMemberships[member] = gset
}
return nil
}
// RemoveUsersFromGroup - remove users from group. If no users are
// given, and the group is empty, deletes the group as well.
func (sys *IAMSys) RemoveUsersFromGroup(group string, members []string) error {
if !sys.Initialized() {
return errServerNotInitialized
}
if sys.usersSysType != MinIOUsersSysType {
return errIAMActionNotAllowed
}
if group == "" {
return errInvalidArgument
}
sys.store.lock()
defer sys.store.unlock()
// Validate that all members exist.
for _, member := range members {
cr, ok := sys.iamUsersMap[member]
if !ok {
return errNoSuchUser
}
if cr.IsTemp() {
return errIAMActionNotAllowed
}
}
gi, ok := sys.iamGroupsMap[group]
if !ok {
return errNoSuchGroup
}
// Check if attempting to delete a non-empty group.
if len(members) == 0 && len(gi.Members) != 0 {
return errGroupNotEmpty
}
if len(members) == 0 {
// len(gi.Members) == 0 here.
// Remove the group from storage. First delete the
// mapped policy. No-mapped-policy case is ignored.
if err := sys.store.deleteMappedPolicy(context.Background(), group, regUser, true); err != nil && err != errNoSuchPolicy {
return err
}
if err := sys.store.deleteGroupInfo(context.Background(), group); err != nil && err != errNoSuchGroup {
return err
}
// Delete from server memory
delete(sys.iamGroupsMap, group)
delete(sys.iamGroupPolicyMap, group)
return nil
}
// Only removing members.
s := set.CreateStringSet(gi.Members...)
d := set.CreateStringSet(members...)
gi.Members = s.Difference(d).ToSlice()
err := sys.store.saveGroupInfo(context.Background(), group, gi)
if err != nil {
return err
}
sys.iamGroupsMap[group] = gi
// update user-group membership map
for _, member := range members {
gset := sys.iamUserGroupMemberships[member]
if gset == nil {
continue
}
gset.Remove(group)
sys.iamUserGroupMemberships[member] = gset
}
return nil
}
// SetGroupStatus - enable/disabled a group
func (sys *IAMSys) SetGroupStatus(group string, enabled bool) error {
if !sys.Initialized() {
return errServerNotInitialized
}
if sys.usersSysType != MinIOUsersSysType {
return errIAMActionNotAllowed
}
sys.store.lock()
defer sys.store.unlock()
if group == "" {
return errInvalidArgument
}
gi, ok := sys.iamGroupsMap[group]
if !ok {
return errNoSuchGroup
}
if enabled {
gi.Status = statusEnabled
} else {
gi.Status = statusDisabled
}
if err := sys.store.saveGroupInfo(context.Background(), group, gi); err != nil {
return err
}
sys.iamGroupsMap[group] = gi
return nil
}
// GetGroupDescription - builds up group description
func (sys *IAMSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err error) {
if !sys.Initialized() {
return gd, errServerNotInitialized
}
ps, err := sys.PolicyDBGet(group, true)
if err != nil {
return gd, err
}
policy := strings.Join(ps, ",")
if sys.usersSysType != MinIOUsersSysType {
return madmin.GroupDesc{
Name: group,
Policy: policy,
}, nil
}
sys.store.rlock()
defer sys.store.runlock()
gi, ok := sys.iamGroupsMap[group]
if !ok {
return gd, errNoSuchGroup
}
return madmin.GroupDesc{
Name: group,
Status: gi.Status,
Members: gi.Members,
Policy: policy,
}, nil
}
// ListGroups - lists groups.
func (sys *IAMSys) ListGroups() (r []string, err error) {
if !sys.Initialized() {
return r, errServerNotInitialized
}
<-sys.configLoaded
sys.store.rlock()
defer sys.store.runlock()
r = make([]string, 0, len(sys.iamGroupsMap))
for k := range sys.iamGroupsMap {
r = append(r, k)
}
if sys.usersSysType == LDAPUsersSysType {
for k := range sys.iamGroupPolicyMap {
r = append(r, k)
}
}
return r, nil
}
// PolicyDBSet - sets a policy for a user or group in the PolicyDB.
func (sys *IAMSys) PolicyDBSet(name, policy string, isGroup bool) error {
if !sys.Initialized() {
return errServerNotInitialized
}
sys.store.lock()
defer sys.store.unlock()
if sys.usersSysType == LDAPUsersSysType {
return sys.policyDBSet(name, policy, stsUser, 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
// given, policies associated with them are included as well.
func (sys *IAMSys) PolicyDBGet(name string, isGroup bool, groups ...string) ([]string, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
if name == "" {
return nil, errInvalidArgument
}
sys.store.rlock()
defer sys.store.runlock()
policies, err := sys.policyDBGet(name, isGroup)
if err != nil {
return nil, err
}
if !isGroup {
for _, group := range groups {
ps, err := sys.policyDBGet(group, true)
if err != nil {
return nil, err
}
policies = append(policies, ps...)
}
}
return policies, nil
}
// This call assumes that caller has the sys.RLock().
//
// If a group is passed, it returns policies associated with the group.
//
// If a user is passed, it returns policies of the user along with any groups
// that the server knows the user is a member of.
//
// In LDAP users mode, the server does not store any group membership
// information in IAM (i.e sys.iam*Map) - this info is stored only in the STS
// generated credentials. Thus we skip looking up group memberships, user map,
// and group map and check the appropriate policy maps directly.
func (sys *IAMSys) policyDBGet(name string, isGroup bool) (policies []string, err error) {
if isGroup {
if sys.usersSysType == MinIOUsersSysType {
g, ok := sys.iamGroupsMap[name]
if !ok {
return nil, errNoSuchGroup
}
// Group is disabled, so we return no policy - this
// ensures the request is denied.
if g.Status == statusDisabled {
return nil, nil
}
}
return sys.iamGroupPolicyMap[name].toSlice(), nil
}
if name == globalActiveCred.AccessKey {
return []string{"consoleAdmin"}, nil
}
// When looking for a user's policies, we also check if the user
// and the groups they are member of are enabled.
var parentName string
u, ok := sys.iamUsersMap[name]
if ok {
if !u.IsValid() {
return nil, nil
}
parentName = u.ParentUser
}
mp, ok := sys.iamUserPolicyMap[name]
if !ok {
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
// actions. The permission of the parent user is checked first
func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser string) bool {
// Now check if we have a subject claim
p, ok := args.Claims[parentClaim]
if ok {
parentInClaim, ok := p.(string)
if !ok {
// Reject malformed/malicious requests.
return false
}
// The parent claim in the session token should be equal
// to the parent detected in the backend
if parentInClaim != parentUser {
return false
}
} else {
// This is needed so a malicious user cannot
// use a leaked session key of another user
// to widen its privileges.
return false
}
// Check policy for this service account.
svcPolicies, err := sys.PolicyDBGet(parentUser, false, args.Groups...)
if err != nil {
logger.LogIf(GlobalContext, err)
return false
}
if len(svcPolicies) == 0 {
return false
}
var availablePolicies []iampolicy.Policy
// Policies were found, evaluate all of them.
sys.store.rlock()
for _, pname := range svcPolicies {
p, found := sys.iamPolicyDocsMap[pname]
if found {
availablePolicies = append(availablePolicies, p)
}
}
sys.store.runlock()
if len(availablePolicies) == 0 {
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.
parentArgs.ConditionValues["username"] = []string{parentUser}
parentArgs.ConditionValues["userid"] = []string{parentUser}
saPolicyClaim, ok := args.Claims[iamPolicyClaimNameSA()]
if !ok {
return false
}
saPolicyClaimStr, ok := saPolicyClaim.(string)
if !ok {
// Sub policy if set, should be a string reject
// malformed/malicious requests.
return false
}
if saPolicyClaimStr == "inherited-policy" {
return combinedPolicy.IsAllowed(parentArgs)
}
// Now check if we have a sessionPolicy.
spolicy, ok := args.Claims[iampolicy.SessionPolicyName]
if !ok {
return false
}
spolicyStr, ok := spolicy.(string)
if !ok {
// Sub policy if set, should be a string reject
// malformed/malicious requests.
return false
}
// Check if policy is parseable.
subPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(spolicyStr)))
if err != nil {
// Log any error in input session policy config.
logger.LogIf(GlobalContext, err)
return false
}
// This can only happen if policy was set but with an empty JSON.
if subPolicy.Version == "" && len(subPolicy.Statements) == 0 {
return combinedPolicy.IsAllowed(parentArgs)
}
if subPolicy.Version == "" {
return false
}
return combinedPolicy.IsAllowed(parentArgs) && subPolicy.IsAllowed(parentArgs)
}
// IsAllowedLDAPSTS - checks for LDAP specific claims and values
func (sys *IAMSys) IsAllowedLDAPSTS(args iampolicy.Args, parentUser string) bool {
parentInClaimIface, ok := args.Claims[ldapUser]
if ok {
parentInClaim, ok := parentInClaimIface.(string)
if !ok {
// ldap parentInClaim name is not a string reject it.
return false
}
if parentInClaim != parentUser {
// ldap claim has been modified maliciously reject it.
return false
}
} else {
// no ldap parentInClaim claim present reject it.
return false
}
// Check policy for this LDAP user.
ldapPolicies, err := sys.PolicyDBGet(parentUser, false, args.Groups...)
if err != nil {
return false
}
if len(ldapPolicies) == 0 {
return false
}
var availablePolicies []iampolicy.Policy
// Policies were found, evaluate all of them.
sys.store.rlock()
for _, pname := range ldapPolicies {
p, found := sys.iamPolicyDocsMap[pname]
if found {
availablePolicies = append(availablePolicies, p)
}
}
sys.store.runlock()
if len(availablePolicies) == 0 {
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)
}
return combinedPolicy.IsAllowed(args)
}
// IsAllowedSTS is meant for STS based temporary credentials,
// which implements claims validation and verification other than
// applying policies.
func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool {
// If it is an LDAP request, check that user and group
// policies allow the request.
if sys.usersSysType == LDAPUsersSysType {
return sys.IsAllowedLDAPSTS(args, parentUser)
}
policies, ok := args.GetPolicies(iamPolicyClaimNameOpenID())
if !ok {
// When claims are set, it should have a policy claim field.
return false
}
// When claims are set, it should have policies as claim.
if policies.IsEmpty() {
// No policy, no access!
return false
}
sys.store.rlock()
defer sys.store.runlock()
// If policy is available for given user, check the policy.
mp, ok := sys.iamUserPolicyMap[args.AccountName]
if !ok {
// No policy set for the user that we can find, no access!
return false
}
if !policies.Equals(mp.policySet()) {
// When claims has a policy, it should match the
// policy of args.AccountName which server remembers.
// if not reject such requests.
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
}
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.
args.ConditionValues["username"] = []string{parentUser}
args.ConditionValues["userid"] = []string{parentUser}
// Now check if we have a sessionPolicy.
hasSessionPolicy, isAllowedSP := isAllowedBySessionPolicy(args)
if hasSessionPolicy {
return isAllowedSP && combinedPolicy.IsAllowed(args)
}
// Sub policy not set, this is most common since subPolicy
// is optional, use the inherited policies.
return combinedPolicy.IsAllowed(args)
}
func isAllowedBySessionPolicy(args iampolicy.Args) (hasSessionPolicy bool, isAllowed bool) {
hasSessionPolicy = false
isAllowed = false
// Now check if we have a sessionPolicy.
spolicy, ok := args.Claims[iampolicy.SessionPolicyName]
if !ok {
return
}
hasSessionPolicy = true
spolicyStr, ok := spolicy.(string)
if !ok {
// Sub policy if set, should be a string reject
// malformed/malicious requests.
return
}
// Check if policy is parseable.
subPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(spolicyStr)))
if err != nil {
// Log any error in input session policy config.
logger.LogIf(GlobalContext, err)
return
}
// Policy without Version string value reject it.
if subPolicy.Version == "" {
return
}
// Sub policy is set and valid.
return hasSessionPolicy, subPolicy.IsAllowed(args)
}
// GetCombinedPolicy returns a combined policy combining all policies
func (sys *IAMSys) GetCombinedPolicy(policies ...string) iampolicy.Policy {
// Policies were found, evaluate all of them.
sys.store.rlock()
defer sys.store.runlock()
var availablePolicies []iampolicy.Policy
for _, pname := range policies {
p, found := sys.iamPolicyDocsMap[pname]
if found {
availablePolicies = append(availablePolicies, p)
}
}
if len(availablePolicies) == 0 {
return iampolicy.Policy{}
}
combinedPolicy := availablePolicies[0]
for i := 1; i < len(availablePolicies); i++ {
combinedPolicy.Statements = append(combinedPolicy.Statements,
availablePolicies[i].Statements...)
}
return combinedPolicy
}
// IsAllowed - checks given policy args is allowed to continue the Rest API.
func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool {
// If opa is configured, use OPA always.
if globalPolicyOPA != nil {
ok, err := globalPolicyOPA.IsAllowed(args)
if err != nil {
logger.LogIf(GlobalContext, err)
}
return ok
}
// Policies don't apply to the owner.
if args.IsOwner {
return true
}
// If the credential is temporary, perform STS related checks.
ok, parentUser, err := sys.IsTempUser(args.AccountName)
if err != nil {
return false
}
if ok {
return sys.IsAllowedSTS(args, parentUser)
}
// If the credential is for a service account, perform related check
ok, parentUser, err = sys.IsServiceAccount(args.AccountName)
if err != nil {
return false
}
if ok {
return sys.IsAllowedServiceAccount(args, parentUser)
}
// Continue with the assumption of a regular user
policies, err := sys.PolicyDBGet(args.AccountName, false, args.Groups...)
if err != nil {
return false
}
if len(policies) == 0 {
// No policy found.
return false
}
// Policies were found, evaluate all of them.
return sys.GetCombinedPolicy(policies...).IsAllowed(args)
}
2018-10-24 20:14:27 -04:00
// Set default canned policies only if not already overridden by users.
func setDefaultCannedPolicies(policies map[string]iampolicy.Policy) {
_, ok := policies["writeonly"]
if !ok {
policies["writeonly"] = iampolicy.WriteOnly
}
_, ok = policies["readonly"]
if !ok {
policies["readonly"] = iampolicy.ReadOnly
}
_, ok = policies["readwrite"]
if !ok {
policies["readwrite"] = iampolicy.ReadWrite
}
_, ok = policies["diagnostics"]
if !ok {
policies["diagnostics"] = iampolicy.AdminDiagnostics
}
_, ok = policies["consoleAdmin"]
if !ok {
policies["consoleAdmin"] = iampolicy.Admin
}
2018-10-24 20:14:27 -04:00
}
// 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
}
// NewIAMSys - creates new config system object.
func NewIAMSys() *IAMSys {
return &IAMSys{
usersSysType: MinIOUsersSysType,
iamUsersMap: make(map[string]auth.Credentials),
iamPolicyDocsMap: make(map[string]iampolicy.Policy),
iamUserPolicyMap: make(map[string]MappedPolicy),
iamGroupPolicyMap: make(map[string]MappedPolicy),
iamGroupsMap: make(map[string]GroupInfo),
iamUserGroupMemberships: make(map[string]set.StringSet),
configLoaded: make(chan struct{}),
}
}