minio/cmd/iam.go

2148 lines
63 KiB
Go

// 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"
"path"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/internal/arn"
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/color"
"github.com/minio/minio/internal/config"
xldap "github.com/minio/minio/internal/config/identity/ldap"
"github.com/minio/minio/internal/config/identity/openid"
idplugin "github.com/minio/minio/internal/config/identity/plugin"
xtls "github.com/minio/minio/internal/config/identity/tls"
"github.com/minio/minio/internal/config/policy/opa"
polplugin "github.com/minio/minio/internal/config/policy/plugin"
xhttp "github.com/minio/minio/internal/http"
xioutil "github.com/minio/minio/internal/ioutil"
"github.com/minio/minio/internal/jwt"
"github.com/minio/minio/internal/logger"
"github.com/minio/pkg/v2/policy"
etcd "go.etcd.io/etcd/client/v3"
)
// 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 (
statusEnabled = "enabled"
statusDisabled = "disabled"
)
const (
embeddedPolicyType = "embedded-policy"
inheritedPolicyType = "inherited-policy"
)
// IAMSys - config system.
type IAMSys struct {
// Need to keep them here to keep alignment - ref: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
// metrics
LastRefreshTimeUnixNano uint64
LastRefreshDurationMilliseconds uint64
TotalRefreshSuccesses uint64
TotalRefreshFailures uint64
sync.Mutex
iamRefreshInterval time.Duration
LDAPConfig xldap.Config // only valid if usersSysType is LDAPUsers
OpenIDConfig openid.Config // only valid if OpenID is configured
STSTLSConfig xtls.Config // only valid if STS TLS is configured
usersSysType UsersSysType
rolesMap map[arn.ARN]string
// Persistence layer for IAM subsystem
store *IAMStoreSys
// 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 (
unknownIAMUserType IAMUserType = iota - 1
regUser
stsUser
svcUser
)
// 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(ctx context.Context, objAPI ObjectLayer, group string) error {
if !sys.Initialized() {
return errServerNotInitialized
}
return sys.store.GroupNotificationHandler(ctx, group)
}
// LoadPolicy - reloads a specific canned policy from backend disks or etcd.
func (sys *IAMSys) LoadPolicy(ctx context.Context, objAPI ObjectLayer, policyName string) error {
if !sys.Initialized() {
return errServerNotInitialized
}
return sys.store.PolicyNotificationHandler(ctx, policyName)
}
// LoadPolicyMapping - loads the mapped policy for a user or group
// from storage into server memory.
func (sys *IAMSys) LoadPolicyMapping(ctx context.Context, objAPI ObjectLayer, userOrGroup string, userType IAMUserType, isGroup bool) error {
if !sys.Initialized() {
return errServerNotInitialized
}
return sys.store.PolicyMappingNotificationHandler(ctx, userOrGroup, isGroup, userType)
}
// LoadUser - reloads a specific user from backend disks or etcd.
func (sys *IAMSys) LoadUser(ctx context.Context, objAPI ObjectLayer, accessKey string, userType IAMUserType) error {
if !sys.Initialized() {
return errServerNotInitialized
}
return sys.store.UserNotificationHandler(ctx, accessKey, userType)
}
// LoadServiceAccount - reloads a specific service account from backend disks or etcd.
func (sys *IAMSys) LoadServiceAccount(ctx context.Context, accessKey string) error {
if !sys.Initialized() {
return errServerNotInitialized
}
return sys.store.UserNotificationHandler(ctx, accessKey, svcUser)
}
// initStore initializes IAM stores
func (sys *IAMSys) initStore(objAPI ObjectLayer, etcdClient *etcd.Client) {
if sys.LDAPConfig.Enabled() {
sys.SetUsersSysType(LDAPUsersSysType)
}
if etcdClient == nil {
sys.store = &IAMStoreSys{newIAMObjectStore(objAPI, sys.usersSysType)}
} else {
sys.store = &IAMStoreSys{newIAMEtcdStore(etcdClient, sys.usersSysType)}
}
}
// Initialized checks 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, policies and policy mappings.
func (sys *IAMSys) Load(ctx context.Context, firstTime bool) error {
loadStartTime := time.Now()
err := sys.store.LoadIAMCache(ctx, firstTime)
if err != nil {
atomic.AddUint64(&sys.TotalRefreshFailures, 1)
return err
}
loadDuration := time.Since(loadStartTime)
atomic.StoreUint64(&sys.LastRefreshDurationMilliseconds, uint64(loadDuration.Milliseconds()))
atomic.StoreUint64(&sys.LastRefreshTimeUnixNano, uint64(loadStartTime.Add(loadDuration).UnixNano()))
atomic.AddUint64(&sys.TotalRefreshSuccesses, 1)
if !globalSiteReplicatorCred.IsValid() {
sa, _, err := sys.getServiceAccount(ctx, siteReplicatorSvcAcc)
if err == nil {
globalSiteReplicatorCred.Set(sa.Credentials)
}
}
if firstTime {
bootstrapTraceMsg(fmt.Sprintf("globalIAMSys.Load(): (duration: %s)", loadDuration))
}
select {
case <-sys.configLoaded:
default:
xioutil.SafeClose(sys.configLoaded)
}
return nil
}
// Init - initializes config system by reading entries from config/iam
func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etcd.Client, iamRefreshInterval time.Duration) {
bootstrapTraceMsg("IAM initialization started")
globalServerConfigMu.RLock()
s := globalServerConfig
globalServerConfigMu.RUnlock()
openidConfig, err := openid.LookupConfig(s,
NewHTTPTransport(), xhttp.DrainBody, globalSite.Region)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
}
// Initialize if LDAP is enabled
ldapConfig, err := xldap.Lookup(s, globalRootCAs)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to parse LDAP configuration: %w", err))
}
stsTLSConfig, err := xtls.Lookup(s[config.IdentityTLSSubSys][config.Default])
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize X.509/TLS STS API: %w", err))
}
if stsTLSConfig.InsecureSkipVerify {
logger.LogIf(ctx, fmt.Errorf("CRITICAL: enabling %s is not recommended in a production environment", xtls.EnvIdentityTLSSkipVerify))
}
authNPluginCfg, err := idplugin.LookupConfig(s[config.IdentityPluginSubSys][config.Default],
NewHTTPTransport(), xhttp.DrainBody, globalSite.Region)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize AuthNPlugin: %w", err))
}
setGlobalAuthNPlugin(idplugin.New(GlobalContext, authNPluginCfg))
authZPluginCfg, err := polplugin.LookupConfig(s, GetDefaultConnSettings(), xhttp.DrainBody)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize AuthZPlugin: %w", err))
}
if authZPluginCfg.URL == nil {
opaCfg, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
NewHTTPTransport(), xhttp.DrainBody)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize AuthZPlugin from legacy OPA config: %w", err))
} else {
authZPluginCfg.URL = opaCfg.URL
authZPluginCfg.AuthToken = opaCfg.AuthToken
authZPluginCfg.Transport = opaCfg.Transport
authZPluginCfg.CloseRespFn = opaCfg.CloseRespFn
}
}
setGlobalAuthZPlugin(polplugin.New(authZPluginCfg))
sys.Lock()
sys.LDAPConfig = ldapConfig
sys.OpenIDConfig = openidConfig
sys.STSTLSConfig = stsTLSConfig
sys.iamRefreshInterval = iamRefreshInterval
// Initialize IAM store
sys.initStore(objAPI, etcdClient)
sys.Unlock()
retryCtx, cancel := context.WithCancel(ctx)
// Indicate to our routine to exit cleanly upon return.
defer cancel()
r := rand.New(rand.NewSource(time.Now().UnixNano()))
// Migrate storage format if needed.
for {
// Migrate IAM configuration, if necessary.
if err := saveIAMFormat(retryCtx, sys.store); 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(time.Second)))
continue
}
logger.LogIf(ctx, fmt.Errorf("IAM sub-system is partially initialized, unable to write the IAM format: %w", err))
return
}
break
}
// Load IAM data from storage.
for {
if err := sys.Load(retryCtx, true); 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(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
}
refreshInterval := sys.iamRefreshInterval
go sys.periodicRoutines(ctx, refreshInterval)
// Load RoleARNs
sys.rolesMap = make(map[arn.ARN]string)
// From OpenID
if riMap := sys.OpenIDConfig.GetRoleInfo(); riMap != nil {
sys.validateAndAddRolePolicyMappings(ctx, riMap)
}
// From AuthN plugin if enabled.
if authn := newGlobalAuthNPluginFn(); authn != nil {
riMap := authn.GetRoleInfo()
sys.validateAndAddRolePolicyMappings(ctx, riMap)
}
sys.printIAMRoles()
bootstrapTraceMsg("finishing IAM loading")
}
func (sys *IAMSys) periodicRoutines(ctx context.Context, baseInterval time.Duration) {
// Watch for IAM config changes for iamStorageWatcher.
watcher, isWatcher := sys.store.IAMStorageAPI.(iamStorageWatcher)
if isWatcher {
go func() {
ch := watcher.watch(ctx, iamConfigPrefix)
for event := range ch {
if err := sys.loadWatchedEvent(ctx, event); err != nil {
// we simply log errors
logger.LogIf(ctx, fmt.Errorf("Failure in loading watch event: %v", err))
}
}
}()
}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
// Add a random interval of up to 20% of the base interval.
randInterval := func() time.Duration {
return time.Duration(r.Float64() * float64(baseInterval) * 0.2)
}
var maxDurationSecondsForLog float64 = 5
timer := time.NewTimer(baseInterval + randInterval())
defer timer.Stop()
for {
select {
case <-timer.C:
// Load all IAM items (except STS creds) periodically.
refreshStart := time.Now()
if err := sys.Load(ctx, false); err != nil {
logger.LogIf(ctx, fmt.Errorf("Failure in periodic refresh for IAM (took %.2fs): %v", time.Since(refreshStart).Seconds(), err))
} else {
took := time.Since(refreshStart).Seconds()
if took > maxDurationSecondsForLog {
// Log if we took a lot of time to load.
logger.Info("IAM refresh took %.2fs", took)
}
}
// The following actions are performed about once in 4 times that
// IAM is refreshed:
if r.Intn(4) == 0 {
// Purge expired STS credentials.
purgeStart := time.Now()
if err := sys.store.PurgeExpiredSTS(ctx); err != nil {
logger.LogIf(ctx, fmt.Errorf("Failure in periodic STS purge for IAM (took %.2fs): %v", time.Since(purgeStart).Seconds(), err))
} else {
took := time.Since(purgeStart).Seconds()
if took > maxDurationSecondsForLog {
// Log if we took a lot of time to load.
logger.Info("IAM expired STS purge took %.2fs", took)
}
}
// Poll and remove accounts for those users who were removed
// from LDAP/OpenID.
if sys.LDAPConfig.Enabled() {
sys.purgeExpiredCredentialsForLDAP(ctx)
sys.updateGroupMembershipsForLDAP(ctx)
}
if sys.OpenIDConfig.ProviderEnabled() {
sys.purgeExpiredCredentialsForExternalSSO(ctx)
}
}
timer.Reset(baseInterval + randInterval())
case <-ctx.Done():
return
}
}
}
func (sys *IAMSys) validateAndAddRolePolicyMappings(ctx context.Context, m map[arn.ARN]string) {
// Validate that policies associated with roles are defined. If
// authZ plugin is set, role policies are just claims sent to
// the plugin and they need not exist.
//
// If some mapped policies do not exist, we print some error
// messages but continue any way - they can be fixed in the
// running server by creating the policies after start up.
for arn, rolePolicies := range m {
specifiedPoliciesSet := newMappedPolicy(rolePolicies).policySet()
validPolicies, _ := sys.store.FilterPolicies(rolePolicies, "")
knownPoliciesSet := newMappedPolicy(validPolicies).policySet()
unknownPoliciesSet := specifiedPoliciesSet.Difference(knownPoliciesSet)
if len(unknownPoliciesSet) > 0 {
authz := newGlobalAuthZPluginFn()
if authz == nil {
// Print a warning that some policies mapped to a role are not defined.
errMsg := fmt.Errorf(
"The policies \"%s\" mapped to role ARN %s are not defined - this role may not work as expected.",
unknownPoliciesSet.ToSlice(), arn.String())
logger.LogIf(ctx, errMsg)
}
}
sys.rolesMap[arn] = rolePolicies
}
}
// Prints IAM role ARNs.
func (sys *IAMSys) printIAMRoles() {
if len(sys.rolesMap) == 0 {
return
}
var arns []string
for arn := range sys.rolesMap {
arns = append(arns, arn.String())
}
sort.Strings(arns)
msgs := make([]string, 0, len(arns))
for _, arn := range arns {
msgs = append(msgs, color.Bold(arn))
}
logger.Info(fmt.Sprintf("%s %s", color.Blue("IAM Roles:"), strings.Join(msgs, " ")))
}
// HasWatcher - returns if the IAM system has a watcher to be notified of
// changes.
func (sys *IAMSys) HasWatcher() bool {
return sys.store.HasWatcher()
}
func (sys *IAMSys) loadWatchedEvent(ctx context.Context, event iamWatchEvent) (err error) {
usersPrefix := strings.HasPrefix(event.keyPath, iamConfigUsersPrefix)
groupsPrefix := strings.HasPrefix(event.keyPath, iamConfigGroupsPrefix)
stsPrefix := strings.HasPrefix(event.keyPath, iamConfigSTSPrefix)
svcPrefix := strings.HasPrefix(event.keyPath, iamConfigServiceAccountsPrefix)
policyPrefix := strings.HasPrefix(event.keyPath, iamConfigPoliciesPrefix)
policyDBUsersPrefix := strings.HasPrefix(event.keyPath, iamConfigPolicyDBUsersPrefix)
policyDBSTSUsersPrefix := strings.HasPrefix(event.keyPath, iamConfigPolicyDBSTSUsersPrefix)
policyDBGroupsPrefix := strings.HasPrefix(event.keyPath, iamConfigPolicyDBGroupsPrefix)
ctx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
defer cancel()
switch {
case usersPrefix:
accessKey := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigUsersPrefix))
err = sys.store.UserNotificationHandler(ctx, accessKey, regUser)
case stsPrefix:
accessKey := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigSTSPrefix))
err = sys.store.UserNotificationHandler(ctx, accessKey, stsUser)
case svcPrefix:
accessKey := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigServiceAccountsPrefix))
err = sys.store.UserNotificationHandler(ctx, accessKey, svcUser)
case groupsPrefix:
group := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigGroupsPrefix))
err = sys.store.GroupNotificationHandler(ctx, group)
case policyPrefix:
policyName := path.Dir(strings.TrimPrefix(event.keyPath, iamConfigPoliciesPrefix))
err = sys.store.PolicyNotificationHandler(ctx, policyName)
case policyDBUsersPrefix:
policyMapFile := strings.TrimPrefix(event.keyPath, iamConfigPolicyDBUsersPrefix)
user := strings.TrimSuffix(policyMapFile, ".json")
err = sys.store.PolicyMappingNotificationHandler(ctx, user, false, regUser)
case policyDBSTSUsersPrefix:
policyMapFile := strings.TrimPrefix(event.keyPath, iamConfigPolicyDBSTSUsersPrefix)
user := strings.TrimSuffix(policyMapFile, ".json")
err = sys.store.PolicyMappingNotificationHandler(ctx, user, false, stsUser)
case policyDBGroupsPrefix:
policyMapFile := strings.TrimPrefix(event.keyPath, iamConfigPolicyDBGroupsPrefix)
user := strings.TrimSuffix(policyMapFile, ".json")
err = sys.store.PolicyMappingNotificationHandler(ctx, user, true, regUser)
}
return err
}
// HasRolePolicy - returns if a role policy is configured for IAM.
func (sys *IAMSys) HasRolePolicy() bool {
return len(sys.rolesMap) > 0
}
// GetRolePolicy - returns policies associated with a role ARN.
func (sys *IAMSys) GetRolePolicy(arnStr string) (arn.ARN, string, error) {
roleArn, err := arn.Parse(arnStr)
if err != nil {
return arn.ARN{}, "", fmt.Errorf("RoleARN parse err: %v", err)
}
rolePolicy, ok := sys.rolesMap[roleArn]
if !ok {
return arn.ARN{}, "", fmt.Errorf("RoleARN %s is not defined.", arnStr)
}
return roleArn, rolePolicy, nil
}
// DeletePolicy - deletes a canned policy from backend. `notifyPeers` is true
// whenever this is called via the API. It is false when called via a
// notification from another peer. This is to avoid infinite loops.
func (sys *IAMSys) DeletePolicy(ctx context.Context, policyName string, notifyPeers bool) error {
if !sys.Initialized() {
return errServerNotInitialized
}
for _, v := range policy.DefaultPolicies {
if v.Name == policyName {
if err := checkConfig(ctx, globalObjectAPI, getPolicyDocPath(policyName)); err != nil && err == errConfigNotFound {
return fmt.Errorf("inbuilt policy `%s` not allowed to be deleted", policyName)
}
}
}
err := sys.store.DeletePolicy(ctx, policyName, !notifyPeers)
if err != nil {
return err
}
if !notifyPeers || sys.HasWatcher() {
return nil
}
// Notify all other MinIO peers to delete policy
for _, nerr := range globalNotificationSys.DeletePolicy(policyName) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
return nil
}
// InfoPolicy - returns the policy definition with some metadata.
func (sys *IAMSys) InfoPolicy(policyName string) (*madmin.PolicyInfo, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
d, err := sys.store.GetPolicyDoc(policyName)
if err != nil {
return nil, err
}
pdata, err := json.Marshal(d.Policy)
if err != nil {
return nil, err
}
return &madmin.PolicyInfo{
PolicyName: policyName,
Policy: pdata,
CreateDate: d.CreateDate,
UpdateDate: d.UpdateDate,
}, nil
}
// ListPolicies - lists all canned policies.
func (sys *IAMSys) ListPolicies(ctx context.Context, bucketName string) (map[string]policy.Policy, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
return sys.store.ListPolicies(ctx, bucketName)
}
// ListPolicyDocs - lists all canned policy docs.
func (sys *IAMSys) ListPolicyDocs(ctx context.Context, bucketName string) (map[string]PolicyDoc, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
return sys.store.ListPolicyDocs(ctx, bucketName)
}
// SetPolicy - sets a new named policy.
func (sys *IAMSys) SetPolicy(ctx context.Context, policyName string, p policy.Policy) (time.Time, error) {
if !sys.Initialized() {
return time.Time{}, errServerNotInitialized
}
updatedAt, err := sys.store.SetPolicy(ctx, policyName, p)
if err != nil {
return updatedAt, err
}
if !sys.HasWatcher() {
// Notify all other MinIO peers to reload policy
for _, nerr := range globalNotificationSys.LoadPolicy(policyName) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
return updatedAt, nil
}
// DeleteUser - delete user (only for long-term users not STS users).
func (sys *IAMSys) DeleteUser(ctx context.Context, accessKey string, notifyPeers bool) error {
if !sys.Initialized() {
return errServerNotInitialized
}
if err := sys.store.DeleteUser(ctx, accessKey, regUser); err != nil {
return err
}
// Notify all other MinIO peers to delete user.
if notifyPeers && !sys.HasWatcher() {
for _, nerr := range globalNotificationSys.DeleteUser(accessKey) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
return nil
}
// 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 ""
}
policies, _ := sys.store.FilterPolicies(policyName, "")
return policies
}
func (sys *IAMSys) notifyForUser(ctx context.Context, accessKey string, isTemp bool) {
// Notify all other MinIO peers to reload user.
if !sys.HasWatcher() {
for _, nerr := range globalNotificationSys.LoadUser(accessKey, isTemp) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
}
// SetTempUser - set temporary user credentials, these credentials have an
// expiry. The permissions for these STS credentials is determined in one of the
// following ways:
//
// - RoleARN - if a role-arn is specified in the request, the STS credential's
// policy is the role's policy.
//
// - inherited from parent - this is the case for AssumeRole API, where the
// parent user is an actual real user with their own (permanent) credentials and
// policy association.
//
// - inherited from "virtual" parent - this is the case for AssumeRoleWithLDAP
// where the parent user is the DN of the actual LDAP user. The parent user
// itself cannot login, but the policy associated with them determines the base
// policy for the STS credential. The policy mapping can be updated by the
// administrator.
//
// - from `Subject.CommonName` field from the STS request for
// AssumeRoleWithCertificate. In this case, the policy for the STS credential
// has the same name as the value of this field.
//
// - from special JWT claim from STS request for AssumeRoleWithOIDC API (when
// not using RoleARN). The claim value can be a string or a list and refers to
// the names of access policies.
//
// For all except the RoleARN case, the implementation is the same - the policy
// for the STS credential is associated with a parent user. For the
// AssumeRoleWithCertificate case, the "virtual" parent user is the value of the
// `Subject.CommonName` field. For the OIDC (without RoleARN) case the "virtual"
// parent is derived as a concatenation of the `sub` and `iss` fields. The
// policies applicable to the STS credential are associated with this "virtual"
// parent.
//
// When a policyName is given to this function, the policy association is
// created and stored in the IAM store. Thus, it should NOT be given for the
// role-arn case (because the role-to-policy mapping is separately stored
// elsewhere), the AssumeRole case (because the parent user is real and their
// policy is associated via policy-set API) and the AssumeRoleWithLDAP case
// (because the policy association is made via policy-set API).
func (sys *IAMSys) SetTempUser(ctx context.Context, accessKey string, cred auth.Credentials, policyName string) (time.Time, error) {
if !sys.Initialized() {
return time.Time{}, errServerNotInitialized
}
if newGlobalAuthZPluginFn() != nil {
// If OPA is set, we do not need to set a policy mapping.
policyName = ""
}
updatedAt, err := sys.store.SetTempUser(ctx, accessKey, cred, policyName)
if err != nil {
return time.Time{}, err
}
sys.notifyForUser(ctx, cred.AccessKey, true)
return updatedAt, nil
}
// ListBucketUsers - list all users who can access this 'bucket'
func (sys *IAMSys) ListBucketUsers(ctx context.Context, bucket string) (map[string]madmin.UserInfo, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
select {
case <-sys.configLoaded:
return sys.store.GetBucketUsers(bucket)
case <-ctx.Done():
return nil, ctx.Err()
}
}
// ListUsers - list all users.
func (sys *IAMSys) ListUsers(ctx context.Context) (map[string]madmin.UserInfo, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
select {
case <-sys.configLoaded:
return sys.store.GetUsers(), nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
// ListLDAPUsers - list LDAP users which has
func (sys *IAMSys) ListLDAPUsers(ctx context.Context) (map[string]madmin.UserInfo, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
if sys.usersSysType != LDAPUsersSysType {
return nil, errIAMActionNotAllowed
}
select {
case <-sys.configLoaded:
ldapUsers := make(map[string]madmin.UserInfo)
for user, policy := range sys.store.GetUsersWithMappedPolicies() {
ldapUsers[user] = madmin.UserInfo{
PolicyName: policy,
Status: madmin.AccountEnabled,
}
}
return ldapUsers, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
// QueryLDAPPolicyEntities - queries policy associations for LDAP users/groups/policies.
func (sys *IAMSys) QueryLDAPPolicyEntities(ctx context.Context, q madmin.PolicyEntitiesQuery) (*madmin.PolicyEntitiesResult, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
if !sys.LDAPConfig.Enabled() {
return nil, errIAMActionNotAllowed
}
select {
case <-sys.configLoaded:
pe := sys.store.ListPolicyMappings(q, sys.LDAPConfig.IsLDAPUserDN, sys.LDAPConfig.IsLDAPGroupDN)
pe.Timestamp = UTCNow()
return &pe, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
// IsTempUser - returns if given key is a temporary user and parent user.
func (sys *IAMSys) IsTempUser(name string) (bool, string, error) {
if !sys.Initialized() {
return false, "", errServerNotInitialized
}
u, found := sys.store.GetUser(name)
if !found {
return false, "", errNoSuchUser
}
cred := u.Credentials
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
}
u, found := sys.store.GetUser(name)
if !found {
return false, "", errNoSuchUser
}
cred := u.Credentials
if cred.IsServiceAccount() {
return true, cred.ParentUser, nil
}
return false, "", nil
}
// GetUserInfo - get info on a user.
func (sys *IAMSys) GetUserInfo(ctx context.Context, name string) (u madmin.UserInfo, err error) {
if !sys.Initialized() {
return u, errServerNotInitialized
}
loadUserCalled := false
select {
case <-sys.configLoaded:
default:
sys.store.LoadUser(ctx, name)
loadUserCalled = true
}
userInfo, err := sys.store.GetUserInfo(name)
if err == errNoSuchUser && !loadUserCalled {
sys.store.LoadUser(ctx, name)
userInfo, err = sys.store.GetUserInfo(name)
}
return userInfo, err
}
// QueryPolicyEntities - queries policy associations for builtin users/groups/policies.
func (sys *IAMSys) QueryPolicyEntities(ctx context.Context, q madmin.PolicyEntitiesQuery) (*madmin.PolicyEntitiesResult, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
select {
case <-sys.configLoaded:
var userPredicate, groupPredicate func(string) bool
if sys.LDAPConfig.Enabled() {
userPredicate = func(s string) bool {
return !sys.LDAPConfig.IsLDAPUserDN(s)
}
groupPredicate = func(s string) bool {
return !sys.LDAPConfig.IsLDAPGroupDN(s)
}
}
pe := sys.store.ListPolicyMappings(q, userPredicate, groupPredicate)
pe.Timestamp = UTCNow()
return &pe, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
// SetUserStatus - sets current user status, supports disabled or enabled.
func (sys *IAMSys) SetUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) (updatedAt time.Time, err error) {
if !sys.Initialized() {
return updatedAt, errServerNotInitialized
}
if sys.usersSysType != MinIOUsersSysType {
return updatedAt, errIAMActionNotAllowed
}
updatedAt, err = sys.store.SetUserStatus(ctx, accessKey, status)
if err != nil {
return
}
sys.notifyForUser(ctx, accessKey, false)
return updatedAt, nil
}
func (sys *IAMSys) notifyForServiceAccount(ctx context.Context, accessKey string) {
// Notify all other Minio peers to reload the service account
if !sys.HasWatcher() {
for _, nerr := range globalNotificationSys.LoadServiceAccount(accessKey) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
}
type newServiceAccountOpts struct {
sessionPolicy *policy.Policy
accessKey string
secretKey string
name, description string
expiration *time.Time
allowSiteReplicatorAccount bool // allow creating internal service account for site-replication.
claims map[string]interface{}
}
// NewServiceAccount - create a new service account
func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, groups []string, opts newServiceAccountOpts) (auth.Credentials, time.Time, error) {
if !sys.Initialized() {
return auth.Credentials{}, time.Time{}, errServerNotInitialized
}
if parentUser == "" {
return auth.Credentials{}, time.Time{}, errInvalidArgument
}
if len(opts.accessKey) > 0 && len(opts.secretKey) == 0 {
return auth.Credentials{}, time.Time{}, auth.ErrNoSecretKeyWithAccessKey
}
if len(opts.secretKey) > 0 && len(opts.accessKey) == 0 {
return auth.Credentials{}, time.Time{}, auth.ErrNoAccessKeyWithSecretKey
}
var policyBuf []byte
if opts.sessionPolicy != nil {
err := opts.sessionPolicy.Validate()
if err != nil {
return auth.Credentials{}, time.Time{}, err
}
policyBuf, err = json.Marshal(opts.sessionPolicy)
if err != nil {
return auth.Credentials{}, time.Time{}, err
}
if len(policyBuf) > 2048 {
return auth.Credentials{}, time.Time{}, errSessionPolicyTooLarge
}
}
// found newly requested service account, to be same as
// parentUser, reject such operations.
if parentUser == opts.accessKey {
return auth.Credentials{}, time.Time{}, errIAMActionNotAllowed
}
if siteReplicatorSvcAcc == opts.accessKey && !opts.allowSiteReplicatorAccount {
return auth.Credentials{}, time.Time{}, errIAMActionNotAllowed
}
m := make(map[string]interface{})
m[parentClaim] = parentUser
if len(policyBuf) > 0 {
m[policy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf)
m[iamPolicyClaimNameSA()] = embeddedPolicyType
} else {
m[iamPolicyClaimNameSA()] = inheritedPolicyType
}
// Add all the necessary claims for the service account.
for k, v := range opts.claims {
_, ok := m[k]
if !ok {
m[k] = v
}
}
var accessKey, secretKey string
var err error
if len(opts.accessKey) > 0 || len(opts.secretKey) > 0 {
accessKey, secretKey = opts.accessKey, opts.secretKey
} else {
accessKey, secretKey, err = auth.GenerateCredentials()
if err != nil {
return auth.Credentials{}, time.Time{}, err
}
}
cred, err := auth.CreateNewCredentialsWithMetadata(accessKey, secretKey, m, secretKey)
if err != nil {
return auth.Credentials{}, time.Time{}, err
}
cred.ParentUser = parentUser
cred.Groups = groups
cred.Status = string(auth.AccountOn)
cred.Name = opts.name
cred.Description = opts.description
if opts.expiration != nil {
expirationInUTC := opts.expiration.UTC()
if err := validateSvcExpirationInUTC(expirationInUTC); err != nil {
return auth.Credentials{}, time.Time{}, err
}
cred.Expiration = expirationInUTC
}
updatedAt, err := sys.store.AddServiceAccount(ctx, cred)
if err != nil {
return auth.Credentials{}, time.Time{}, err
}
sys.notifyForServiceAccount(ctx, cred.AccessKey)
return cred, updatedAt, nil
}
type updateServiceAccountOpts struct {
sessionPolicy *policy.Policy
secretKey string
status string
name, description string
expiration *time.Time
}
// UpdateServiceAccount - edit a service account
func (sys *IAMSys) UpdateServiceAccount(ctx context.Context, accessKey string, opts updateServiceAccountOpts) (updatedAt time.Time, err error) {
if !sys.Initialized() {
return updatedAt, errServerNotInitialized
}
updatedAt, err = sys.store.UpdateServiceAccount(ctx, accessKey, opts)
if err != nil {
return updatedAt, err
}
sys.notifyForServiceAccount(ctx, accessKey)
return updatedAt, nil
}
// ListServiceAccounts - lists all service accounts associated to a specific user
func (sys *IAMSys) ListServiceAccounts(ctx context.Context, accessKey string) ([]auth.Credentials, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
select {
case <-sys.configLoaded:
return sys.store.ListServiceAccounts(ctx, accessKey)
case <-ctx.Done():
return nil, ctx.Err()
}
}
// ListTempAccounts - lists all temporary service accounts associated to a specific user
func (sys *IAMSys) ListTempAccounts(ctx context.Context, accessKey string) ([]UserIdentity, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
select {
case <-sys.configLoaded:
return sys.store.ListTempAccounts(ctx, accessKey)
case <-ctx.Done():
return nil, ctx.Err()
}
}
// ListSTSAccounts - lists all STS accounts associated to a specific user
func (sys *IAMSys) ListSTSAccounts(ctx context.Context, accessKey string) ([]auth.Credentials, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
select {
case <-sys.configLoaded:
return sys.store.ListSTSAccounts(ctx, accessKey)
case <-ctx.Done():
return nil, ctx.Err()
}
}
// GetServiceAccount - wrapper method to get information about a service account
func (sys *IAMSys) GetServiceAccount(ctx context.Context, accessKey string) (auth.Credentials, *policy.Policy, error) {
sa, embeddedPolicy, err := sys.getServiceAccount(ctx, accessKey)
if err != nil {
return auth.Credentials{}, nil, err
}
// Hide secret & session keys
sa.Credentials.SecretKey = ""
sa.Credentials.SessionToken = ""
return sa.Credentials, embeddedPolicy, nil
}
func (sys *IAMSys) getServiceAccount(ctx context.Context, accessKey string) (UserIdentity, *policy.Policy, error) {
sa, jwtClaims, err := sys.getAccountWithClaims(ctx, accessKey)
if err != nil {
if err == errNoSuchAccount {
return UserIdentity{}, nil, errNoSuchServiceAccount
}
return UserIdentity{}, nil, err
}
if !sa.Credentials.IsServiceAccount() {
return UserIdentity{}, nil, errNoSuchServiceAccount
}
var embeddedPolicy *policy.Policy
pt, ptok := jwtClaims.Lookup(iamPolicyClaimNameSA())
sp, spok := jwtClaims.Lookup(policy.SessionPolicyName)
if ptok && spok && pt == embeddedPolicyType {
policyBytes, err := base64.StdEncoding.DecodeString(sp)
if err != nil {
return UserIdentity{}, nil, err
}
embeddedPolicy, err = policy.ParseConfig(bytes.NewReader(policyBytes))
if err != nil {
return UserIdentity{}, nil, err
}
}
return sa, embeddedPolicy, nil
}
// GetTemporaryAccount - wrapper method to get information about a temporary account
func (sys *IAMSys) GetTemporaryAccount(ctx context.Context, accessKey string) (auth.Credentials, *policy.Policy, error) {
tmpAcc, embeddedPolicy, err := sys.getTempAccount(ctx, accessKey)
if err != nil {
return auth.Credentials{}, nil, err
}
// Hide secret & session keys
tmpAcc.Credentials.SecretKey = ""
tmpAcc.Credentials.SessionToken = ""
return tmpAcc.Credentials, embeddedPolicy, nil
}
func (sys *IAMSys) getTempAccount(ctx context.Context, accessKey string) (UserIdentity, *policy.Policy, error) {
tmpAcc, claims, err := sys.getAccountWithClaims(ctx, accessKey)
if err != nil {
if err == errNoSuchAccount {
return UserIdentity{}, nil, errNoSuchTempAccount
}
return UserIdentity{}, nil, err
}
if !tmpAcc.Credentials.IsTemp() {
return UserIdentity{}, nil, errNoSuchTempAccount
}
var embeddedPolicy *policy.Policy
sp, spok := claims.Lookup(policy.SessionPolicyName)
if spok {
policyBytes, err := base64.StdEncoding.DecodeString(sp)
if err != nil {
return UserIdentity{}, nil, err
}
embeddedPolicy, err = policy.ParseConfig(bytes.NewReader(policyBytes))
if err != nil {
return UserIdentity{}, nil, err
}
}
return tmpAcc, embeddedPolicy, nil
}
// getAccountWithClaims - gets information about an account with claims
func (sys *IAMSys) getAccountWithClaims(ctx context.Context, accessKey string) (UserIdentity, *jwt.MapClaims, error) {
if !sys.Initialized() {
return UserIdentity{}, nil, errServerNotInitialized
}
acc, ok := sys.store.GetUser(accessKey)
if !ok {
return UserIdentity{}, nil, errNoSuchAccount
}
jwtClaims, err := extractJWTClaims(acc)
if err != nil {
return UserIdentity{}, nil, err
}
return acc, jwtClaims, nil
}
// GetClaimsForSvcAcc - gets the claims associated with the service account.
func (sys *IAMSys) GetClaimsForSvcAcc(ctx context.Context, accessKey string) (map[string]interface{}, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
if sys.usersSysType != LDAPUsersSysType {
return nil, nil
}
sa, ok := sys.store.GetUser(accessKey)
if !ok || !sa.Credentials.IsServiceAccount() {
return nil, errNoSuchServiceAccount
}
jwtClaims, err := extractJWTClaims(sa)
if err != nil {
return nil, err
}
return jwtClaims.Map(), nil
}
// DeleteServiceAccount - delete a service account
func (sys *IAMSys) DeleteServiceAccount(ctx context.Context, accessKey string, notifyPeers bool) error {
if !sys.Initialized() {
return errServerNotInitialized
}
sa, ok := sys.store.GetUser(accessKey)
if !ok || !sa.Credentials.IsServiceAccount() {
return nil
}
if err := sys.store.DeleteUser(ctx, accessKey, svcUser); err != nil {
return err
}
if notifyPeers && !sys.HasWatcher() {
for _, nerr := range globalNotificationSys.DeleteServiceAccount(accessKey) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
return nil
}
// CreateUser - create new user credentials and policy, if user already exists
// they shall be rewritten with new inputs.
func (sys *IAMSys) CreateUser(ctx context.Context, accessKey string, ureq madmin.AddOrUpdateUserReq) (updatedAt time.Time, err error) {
if !sys.Initialized() {
return updatedAt, errServerNotInitialized
}
if sys.usersSysType != MinIOUsersSysType {
return updatedAt, errIAMActionNotAllowed
}
if !auth.IsAccessKeyValid(accessKey) {
return updatedAt, auth.ErrInvalidAccessKeyLength
}
if !auth.IsSecretKeyValid(ureq.SecretKey) {
return updatedAt, auth.ErrInvalidSecretKeyLength
}
updatedAt, err = sys.store.AddUser(ctx, accessKey, ureq)
if err != nil {
return updatedAt, err
}
sys.notifyForUser(ctx, accessKey, false)
return updatedAt, nil
}
// SetUserSecretKey - sets user secret key
func (sys *IAMSys) SetUserSecretKey(ctx context.Context, 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
}
return sys.store.UpdateUserSecretKey(ctx, accessKey, secretKey)
}
// purgeExpiredCredentialsForExternalSSO - validates if local credentials are still valid
// by checking remote IDP if the relevant users are still active and present.
func (sys *IAMSys) purgeExpiredCredentialsForExternalSSO(ctx context.Context) {
parentUsersMap := sys.store.GetAllParentUsers()
var expiredUsers []string
for parentUser, puInfo := range parentUsersMap {
// There are multiple role ARNs for parent user only when there
// are multiple openid provider configurations with the same ID
// provider. We lookup the provider associated with some one of
// the roleARNs to check if the user still exists. If they don't
// we can safely remove credentials for this parent user
// associated with any of the provider configurations.
//
// If there is no roleARN mapped to the user, the user may be
// coming from a policy claim based openid provider.
roleArns := puInfo.roleArns.ToSlice()
var roleArn string
if len(roleArns) == 0 {
logger.LogIf(GlobalContext,
fmt.Errorf("parentUser: %s had no roleArns mapped!", parentUser))
continue
}
roleArn = roleArns[0]
u, err := sys.OpenIDConfig.LookupUser(roleArn, puInfo.subClaimValue)
if err != nil {
logger.LogIf(GlobalContext, err)
continue
}
// If user is set to "disabled", we will remove them
// subsequently.
if !u.Enabled {
expiredUsers = append(expiredUsers, parentUser)
}
}
// We ignore any errors
_ = sys.store.DeleteUsers(ctx, expiredUsers)
}
// purgeExpiredCredentialsForLDAP - validates if local credentials are still
// valid by checking LDAP server if the relevant users are still present.
func (sys *IAMSys) purgeExpiredCredentialsForLDAP(ctx context.Context) {
parentUsers := sys.store.GetAllParentUsers()
var allDistNames []string
for parentUser := range parentUsers {
if !sys.LDAPConfig.IsLDAPUserDN(parentUser) {
continue
}
allDistNames = append(allDistNames, parentUser)
}
expiredUsers, err := sys.LDAPConfig.GetNonEligibleUserDistNames(allDistNames)
if err != nil {
// Log and return on error - perhaps it'll work the next time.
logger.LogIf(GlobalContext, err)
return
}
// We ignore any errors
_ = sys.store.DeleteUsers(ctx, expiredUsers)
}
// updateGroupMembershipsForLDAP - updates the list of groups associated with the credential.
func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
// 1. Collect all LDAP users with active creds.
allCreds := sys.store.GetSTSAndServiceAccounts()
// List of unique LDAP (parent) user DNs that have active creds
var parentUsers []string
// Map of LDAP user to list of active credential objects
parentUserToCredsMap := make(map[string][]auth.Credentials)
// DN to ldap username mapping for each LDAP user
parentUserToLDAPUsernameMap := make(map[string]string)
for _, cred := range allCreds {
// Expired credentials don't need parent user updates.
if cred.IsExpired() {
continue
}
if !sys.LDAPConfig.IsLDAPUserDN(cred.ParentUser) {
continue
}
// Check if this is the first time we are
// encountering this LDAP user.
if _, ok := parentUserToCredsMap[cred.ParentUser]; !ok {
// Try to find the ldapUsername for this
// parentUser by extracting JWT claims
var (
jwtClaims *jwt.MapClaims
err error
)
if cred.SessionToken == "" {
continue
}
if cred.IsServiceAccount() {
jwtClaims, err = auth.ExtractClaims(cred.SessionToken, cred.SecretKey)
if err != nil {
jwtClaims, err = auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey)
}
} else {
var secretKey string
secretKey, err = getTokenSigningKey()
if err != nil {
continue
}
jwtClaims, err = auth.ExtractClaims(cred.SessionToken, secretKey)
}
if err != nil {
// skip this cred - session token seems invalid
continue
}
ldapUsername, ok := jwtClaims.Lookup(ldapUserN)
if !ok {
// skip this cred - we dont have the
// username info needed
continue
}
// Collect each new cred.ParentUser into parentUsers
parentUsers = append(parentUsers, cred.ParentUser)
// Update the ldapUsernameMap
parentUserToLDAPUsernameMap[cred.ParentUser] = ldapUsername
}
parentUserToCredsMap[cred.ParentUser] = append(parentUserToCredsMap[cred.ParentUser], cred)
}
// 2. Query LDAP server for groups of the LDAP users collected.
updatedGroups, err := sys.LDAPConfig.LookupGroupMemberships(parentUsers, parentUserToLDAPUsernameMap)
if err != nil {
// Log and return on error - perhaps it'll work the next time.
logger.LogIf(GlobalContext, err)
return
}
// 3. Update creds for those users whose groups are changed
for _, parentUser := range parentUsers {
currGroupsSet := updatedGroups[parentUser]
currGroups := currGroupsSet.ToSlice()
for _, cred := range parentUserToCredsMap[parentUser] {
gSet := set.CreateStringSet(cred.Groups...)
if gSet.Equals(currGroupsSet) {
// No change to groups memberships for this
// credential.
continue
}
// Expired credentials don't need group membership updates.
if cred.IsExpired() {
continue
}
cred.Groups = currGroups
if err := sys.store.UpdateUserIdentity(ctx, cred); err != nil {
// Log and continue error - perhaps it'll work the next time.
logger.LogIf(GlobalContext, err)
}
}
}
}
// GetUser - get user credentials
func (sys *IAMSys) GetUser(ctx context.Context, accessKey string) (u UserIdentity, ok bool) {
if !sys.Initialized() {
return u, false
}
if accessKey == globalActiveCred.AccessKey {
return newUserIdentity(globalActiveCred), true
}
loadUserCalled := false
select {
case <-sys.configLoaded:
default:
sys.store.LoadUser(ctx, accessKey)
loadUserCalled = true
}
u, ok = sys.store.GetUser(accessKey)
if !ok && !loadUserCalled {
sys.store.LoadUser(ctx, accessKey)
u, ok = sys.store.GetUser(accessKey)
}
return u, ok && u.Credentials.IsValid()
}
// Notify all other MinIO peers to load group.
func (sys *IAMSys) notifyForGroup(ctx context.Context, group string) {
if !sys.HasWatcher() {
for _, nerr := range globalNotificationSys.LoadGroup(group) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
}
// 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(ctx context.Context, group string, members []string) (updatedAt time.Time, err error) {
if !sys.Initialized() {
return updatedAt, errServerNotInitialized
}
if sys.usersSysType != MinIOUsersSysType {
return updatedAt, errIAMActionNotAllowed
}
updatedAt, err = sys.store.AddUsersToGroup(ctx, group, members)
if err != nil {
return updatedAt, err
}
sys.notifyForGroup(ctx, group)
return updatedAt, 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(ctx context.Context, group string, members []string) (updatedAt time.Time, err error) {
if !sys.Initialized() {
return updatedAt, errServerNotInitialized
}
if sys.usersSysType != MinIOUsersSysType {
return updatedAt, errIAMActionNotAllowed
}
updatedAt, err = sys.store.RemoveUsersFromGroup(ctx, group, members)
if err != nil {
return updatedAt, err
}
sys.notifyForGroup(ctx, group)
return updatedAt, nil
}
// SetGroupStatus - enable/disabled a group
func (sys *IAMSys) SetGroupStatus(ctx context.Context, group string, enabled bool) (updatedAt time.Time, err error) {
if !sys.Initialized() {
return updatedAt, errServerNotInitialized
}
if sys.usersSysType != MinIOUsersSysType {
return updatedAt, errIAMActionNotAllowed
}
updatedAt, err = sys.store.SetGroupStatus(ctx, group, enabled)
if err != nil {
return updatedAt, err
}
sys.notifyForGroup(ctx, group)
return updatedAt, nil
}
// GetGroupDescription - builds up group description
func (sys *IAMSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err error) {
if !sys.Initialized() {
return gd, errServerNotInitialized
}
return sys.store.GetGroupDescription(group)
}
// ListGroups - lists groups.
func (sys *IAMSys) ListGroups(ctx context.Context) (r []string, err error) {
if !sys.Initialized() {
return r, errServerNotInitialized
}
select {
case <-sys.configLoaded:
return sys.store.ListGroups(ctx)
case <-ctx.Done():
return nil, ctx.Err()
}
}
// PolicyDBSet - sets a policy for a user or group in the PolicyDB - the user doesn't have to exist since sometimes they are virtuals
func (sys *IAMSys) PolicyDBSet(ctx context.Context, name, policy string, userType IAMUserType, isGroup bool) (updatedAt time.Time, err error) {
if !sys.Initialized() {
return updatedAt, errServerNotInitialized
}
updatedAt, err = sys.store.PolicyDBSet(ctx, name, policy, userType, isGroup)
if err != nil {
return
}
// Notify all other MinIO peers to reload policy
if !sys.HasWatcher() {
for _, nerr := range globalNotificationSys.LoadPolicyMapping(name, userType, isGroup) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
return updatedAt, nil
}
// PolicyDBUpdateBuiltin - adds or removes policies from a user or a group
// verified to be an internal IDP user.
func (sys *IAMSys) PolicyDBUpdateBuiltin(ctx context.Context, isAttach bool,
r madmin.PolicyAssociationReq,
) (updatedAt time.Time, addedOrRemoved, effectivePolicies []string, err error) {
if !sys.Initialized() {
err = errServerNotInitialized
return
}
userOrGroup := r.User
var isGroup bool
if userOrGroup == "" {
isGroup = true
userOrGroup = r.Group
}
if isGroup {
_, err = sys.GetGroupDescription(userOrGroup)
if err != nil {
return
}
} else {
var isTemp bool
isTemp, _, err = sys.IsTempUser(userOrGroup)
if err != nil && err != errNoSuchUser {
return
}
if isTemp {
err = errIAMActionNotAllowed
return
}
// When the user is root credential you are not allowed to
// add policies for root user.
if userOrGroup == globalActiveCred.AccessKey {
err = errIAMActionNotAllowed
return
}
// Validate that user exists.
var userExists bool
_, userExists = sys.GetUser(ctx, userOrGroup)
if !userExists {
err = errNoSuchUser
return
}
}
updatedAt, addedOrRemoved, effectivePolicies, err = sys.store.PolicyDBUpdate(ctx, userOrGroup, isGroup,
regUser, r.Policies, isAttach)
if err != nil {
return
}
// Notify all other MinIO peers to reload policy
if !sys.HasWatcher() {
for _, nerr := range globalNotificationSys.LoadPolicyMapping(userOrGroup, regUser, isGroup) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
Type: madmin.SRIAMItemPolicyMapping,
PolicyMapping: &madmin.SRPolicyMapping{
UserOrGroup: userOrGroup,
UserType: int(regUser),
IsGroup: isGroup,
Policy: strings.Join(effectivePolicies, ","),
},
UpdatedAt: updatedAt,
}))
return
}
// PolicyDBUpdateLDAP - adds or removes policies from a user or a group verified
// to be in the LDAP directory.
func (sys *IAMSys) PolicyDBUpdateLDAP(ctx context.Context, isAttach bool,
r madmin.PolicyAssociationReq,
) (updatedAt time.Time, addedOrRemoved, effectivePolicies []string, err error) {
if !sys.Initialized() {
err = errServerNotInitialized
return
}
var dn string
var isGroup bool
if r.User != "" {
dn, err = sys.LDAPConfig.DoesUsernameExist(r.User)
if err != nil {
logger.LogIf(ctx, err)
return
}
if dn == "" {
// Still attempt to detach if provided user is a DN.
if !isAttach && sys.LDAPConfig.IsLDAPUserDN(r.User) {
dn = r.User
} else {
err = errNoSuchUser
return
}
}
isGroup = false
} else {
if isAttach {
var exists bool
if exists, err = sys.LDAPConfig.DoesGroupDNExist(r.Group); err != nil {
logger.LogIf(ctx, err)
return
} else if !exists {
err = errNoSuchGroup
return
}
}
dn = r.Group
isGroup = true
}
userType := stsUser
updatedAt, addedOrRemoved, effectivePolicies, err = sys.store.PolicyDBUpdate(ctx, dn, isGroup,
userType, r.Policies, isAttach)
if err != nil {
return
}
// Notify all other MinIO peers to reload policy
if !sys.HasWatcher() {
for _, nerr := range globalNotificationSys.LoadPolicyMapping(dn, userType, isGroup) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
Type: madmin.SRIAMItemPolicyMapping,
PolicyMapping: &madmin.SRPolicyMapping{
UserOrGroup: dn,
UserType: int(userType),
IsGroup: isGroup,
Policy: strings.Join(effectivePolicies, ","),
},
UpdatedAt: updatedAt,
}))
return
}
// 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, groups ...string) ([]string, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
return sys.store.PolicyDBGet(name, groups...)
}
const sessionPolicyNameExtracted = policy.SessionPolicyName + "-extracted"
// 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 policy.Args, parentUser string) bool {
// Verify if the parent claim matches the parentUser.
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
}
isOwnerDerived := parentUser == globalActiveCred.AccessKey
var err error
var svcPolicies []string
roleArn := args.GetRoleArn()
switch {
case isOwnerDerived:
// All actions are allowed by default and no policy evaluation is
// required.
case roleArn != "":
arn, err := arn.Parse(roleArn)
if err != nil {
logger.LogIf(GlobalContext, fmt.Errorf("error parsing role ARN %s: %v", roleArn, err))
return false
}
svcPolicies = newMappedPolicy(sys.rolesMap[arn]).toSlice()
default:
// Check policy for parent user of service account.
svcPolicies, err = sys.PolicyDBGet(parentUser, args.Groups...)
if err != nil {
logger.LogIf(GlobalContext, err)
return false
}
// Finally, if there is no parent policy, check if a policy claim is
// present.
if len(svcPolicies) == 0 {
policySet, _ := policy.GetPoliciesFromClaims(args.Claims, iamPolicyClaimNameOpenID())
svcPolicies = policySet.ToSlice()
}
}
// Defensive code: Do not allow any operation if no policy is found.
if !isOwnerDerived && len(svcPolicies) == 0 {
return false
}
var combinedPolicy policy.Policy
// Policies were found, evaluate all of them.
if !isOwnerDerived {
availablePoliciesStr, c := sys.store.FilterPolicies(strings.Join(svcPolicies, ","), "")
if availablePoliciesStr == "" {
return false
}
combinedPolicy = c
}
parentArgs := args
parentArgs.AccountName = 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 == inheritedPolicyType {
return isOwnerDerived || combinedPolicy.IsAllowed(parentArgs)
}
// 3. If an inline session-policy is present, evaluate it.
hasSessionPolicy, isAllowedSP := isAllowedBySessionPolicyForServiceAccount(args)
if hasSessionPolicy {
return isAllowedSP && (isOwnerDerived || combinedPolicy.IsAllowed(parentArgs))
}
// Sub policy not set. Evaluate only the parent policies.
return (isOwnerDerived || combinedPolicy.IsAllowed(parentArgs))
}
// IsAllowedSTS is meant for STS based temporary credentials,
// which implements claims validation and verification other than
// applying policies.
func (sys *IAMSys) IsAllowedSTS(args policy.Args, parentUser string) bool {
// 1. Determine mapped policies
isOwnerDerived := parentUser == globalActiveCred.AccessKey
var policies []string
roleArn := args.GetRoleArn()
switch {
case isOwnerDerived:
// All actions are allowed by default and no policy evaluation is
// required.
case roleArn != "":
// If a roleARN is present, the role policy is applied.
arn, err := arn.Parse(roleArn)
if err != nil {
logger.LogIf(GlobalContext, fmt.Errorf("error parsing role ARN %s: %v", roleArn, err))
return false
}
policies = newMappedPolicy(sys.rolesMap[arn]).toSlice()
default:
// Otherwise, inherit parent user's policy
var err error
policies, err = sys.store.PolicyDBGet(parentUser, args.Groups...)
if err != nil {
logger.LogIf(GlobalContext, fmt.Errorf("error fetching policies on %s: %v", parentUser, err))
return false
}
// Finally, if there is no parent policy, check if a policy claim is
// present in the session token.
if len(policies) == 0 {
// If there is no parent policy mapping, we fall back to
// using policy claim from JWT.
policySet, ok := args.GetPolicies(iamPolicyClaimNameOpenID())
if !ok {
// When claims are set, it should have a policy claim field.
return false
}
policies = policySet.ToSlice()
}
}
// Defensive code: Do not allow any operation if no policy is found in the session token
if !isOwnerDerived && len(policies) == 0 {
return false
}
// 2. Combine the mapped policies into a single combined policy.
var combinedPolicy policy.Policy
if !isOwnerDerived {
var err error
combinedPolicy, err = sys.store.GetPolicy(strings.Join(policies, ","))
if errors.Is(err, errNoSuchPolicy) {
for _, pname := range policies {
_, err := sys.store.GetPolicy(pname)
if errors.Is(err, errNoSuchPolicy) {
// all policies presented in the claim should exist
logger.LogIf(GlobalContext, fmt.Errorf("expected policy (%s) missing from the JWT claim %s, rejecting the request", pname, iamPolicyClaimNameOpenID()))
return false
}
}
logger.LogIf(GlobalContext, fmt.Errorf("all policies were unexpectedly present!"))
return false
}
}
// 3. If an inline session-policy is present, evaluate it.
// Now check if we have a sessionPolicy.
hasSessionPolicy, isAllowedSP := isAllowedBySessionPolicy(args)
if hasSessionPolicy {
return isAllowedSP && (isOwnerDerived || combinedPolicy.IsAllowed(args))
}
// Sub policy not set, this is most common since subPolicy
// is optional, use the inherited policies.
return isOwnerDerived || combinedPolicy.IsAllowed(args)
}
func isAllowedBySessionPolicyForServiceAccount(args policy.Args) (hasSessionPolicy bool, isAllowed bool) {
hasSessionPolicy = false
isAllowed = false
// Now check if we have a sessionPolicy.
spolicy, ok := args.Claims[sessionPolicyNameExtracted]
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 := policy.ParseConfig(bytes.NewReader([]byte(spolicyStr)))
if err != nil {
// Log any error in input session policy config.
logger.LogIf(GlobalContext, err)
return
}
// SPECIAL CASE: For service accounts, any valid JSON is allowed as a
// policy, regardless of whether the number of statements is 0, this
// includes `null`, `{}` and `{"Statement": null}`. In fact, MinIO Console
// sends `null` when no policy is set and the intended behavior is that the
// service account should inherit parent policy.
//
// However, for a policy like `{"Statement":[]}`, the intention is to not
// provide any permissions via the session policy - i.e. the service account
// can do nothing (such a JSON could be generated by an external application
// as the policy for the service account). Inheriting the parent policy in
// such a case, is a security issue. Ideally, we should not allow such
// behavior, but for compatibility with the Console, we currently allow it.
//
// TODO:
//
// 1. fix console behavior and allow this inheritance for service accounts
// created before a certain (TBD) future date.
//
// 2. do not allow empty statement policies for service accounts.
if subPolicy.Version == "" && subPolicy.Statements == nil && subPolicy.ID == "" {
hasSessionPolicy = false
return
}
// As the session policy exists, even if the parent is the root account, it
// must be restricted by it. So, we set `.IsOwner` to false here
// unconditionally.
sessionPolicyArgs := args
sessionPolicyArgs.IsOwner = false
// Sub policy is set and valid.
return hasSessionPolicy, subPolicy.IsAllowed(sessionPolicyArgs)
}
func isAllowedBySessionPolicy(args policy.Args) (hasSessionPolicy bool, isAllowed bool) {
hasSessionPolicy = false
isAllowed = false
// Now check if we have a sessionPolicy.
spolicy, ok := args.Claims[sessionPolicyNameExtracted]
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 := policy.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
}
// As the session policy exists, even if the parent is the root account, it
// must be restricted by it. So, we set `.IsOwner` to false here
// unconditionally.
sessionPolicyArgs := args
sessionPolicyArgs.IsOwner = false
// Sub policy is set and valid.
return hasSessionPolicy, subPolicy.IsAllowed(sessionPolicyArgs)
}
// GetCombinedPolicy returns a combined policy combining all policies
func (sys *IAMSys) GetCombinedPolicy(policies ...string) policy.Policy {
_, policy := sys.store.FilterPolicies(strings.Join(policies, ","), "")
return policy
}
// doesPolicyAllow - checks if the given policy allows the passed action with given args. This is rarely needed.
// Notice there is no account name involved, so this is a dangerous function.
func (sys *IAMSys) doesPolicyAllow(policy string, args policy.Args) bool {
// Policies were found, evaluate all of them.
return sys.GetCombinedPolicy(policy).IsAllowed(args)
}
// IsAllowed - checks given policy args is allowed to continue the Rest API.
func (sys *IAMSys) IsAllowed(args policy.Args) bool {
// If opa is configured, use OPA always.
if authz := newGlobalAuthZPluginFn(); authz != nil {
ok, err := authz.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, 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)
}
// SetUsersSysType - sets the users system type, regular or LDAP.
func (sys *IAMSys) SetUsersSysType(t UsersSysType) {
sys.usersSysType = t
}
// GetUsersSysType - returns the users system type for this IAM
func (sys *IAMSys) GetUsersSysType() UsersSysType {
return sys.usersSysType
}
// NewIAMSys - creates new config system object.
func NewIAMSys() *IAMSys {
return &IAMSys{
usersSysType: MinIOUsersSysType,
configLoaded: make(chan struct{}),
}
}