2021-04-18 15:41:13 -04:00
|
|
|
// 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/>.
|
2019-08-08 18:10:04 -04:00
|
|
|
|
|
|
|
package cmd
|
|
|
|
|
|
|
|
import (
|
2023-04-21 16:56:08 -04:00
|
|
|
"bytes"
|
2019-08-08 18:10:04 -04:00
|
|
|
"context"
|
2022-11-14 10:15:46 -05:00
|
|
|
"errors"
|
2022-03-27 21:48:01 -04:00
|
|
|
"fmt"
|
2020-12-19 12:36:37 -05:00
|
|
|
"path"
|
2019-08-08 18:10:04 -04:00
|
|
|
"strings"
|
|
|
|
"sync"
|
2024-03-01 19:21:34 -05:00
|
|
|
"time"
|
2020-07-17 20:41:29 -04:00
|
|
|
"unicode/utf8"
|
2019-08-08 18:10:04 -04:00
|
|
|
|
2021-05-11 05:02:32 -04:00
|
|
|
jsoniter "github.com/json-iterator/go"
|
2023-06-19 20:53:08 -04:00
|
|
|
"github.com/minio/madmin-go/v3"
|
2021-06-01 17:59:40 -04:00
|
|
|
"github.com/minio/minio/internal/config"
|
2024-01-28 13:04:17 -05:00
|
|
|
xioutil "github.com/minio/minio/internal/ioutil"
|
2021-06-01 17:59:40 -04:00
|
|
|
"github.com/minio/minio/internal/kms"
|
2024-06-10 14:45:50 -04:00
|
|
|
"github.com/minio/minio/internal/logger"
|
2024-03-26 14:12:57 -04:00
|
|
|
"github.com/puzpuzpuz/xsync/v3"
|
2019-08-08 18:10:04 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
// IAMObjectStore implements IAMStorageAPI
|
|
|
|
type IAMObjectStore struct {
|
2021-11-03 22:47:49 -04:00
|
|
|
// Protect access to storage within the current server.
|
2019-08-08 18:10:04 -04:00
|
|
|
sync.RWMutex
|
|
|
|
|
2021-11-03 22:47:49 -04:00
|
|
|
*iamCache
|
|
|
|
|
|
|
|
usersSysType UsersSysType
|
|
|
|
|
2019-08-08 18:10:04 -04:00
|
|
|
objAPI ObjectLayer
|
|
|
|
}
|
|
|
|
|
2021-11-03 22:47:49 -04:00
|
|
|
func newIAMObjectStore(objAPI ObjectLayer, usersSysType UsersSysType) *IAMObjectStore {
|
|
|
|
return &IAMObjectStore{
|
|
|
|
iamCache: newIamCache(),
|
|
|
|
objAPI: objAPI,
|
|
|
|
usersSysType: usersSysType,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (iamOS *IAMObjectStore) rlock() *iamCache {
|
|
|
|
iamOS.RLock()
|
|
|
|
return iamOS.iamCache
|
|
|
|
}
|
|
|
|
|
|
|
|
func (iamOS *IAMObjectStore) runlock() {
|
|
|
|
iamOS.RUnlock()
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
|
|
|
|
2021-11-03 22:47:49 -04:00
|
|
|
func (iamOS *IAMObjectStore) lock() *iamCache {
|
2020-04-07 17:26:39 -04:00
|
|
|
iamOS.Lock()
|
2021-11-03 22:47:49 -04:00
|
|
|
return iamOS.iamCache
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
|
|
|
|
2020-04-07 17:26:39 -04:00
|
|
|
func (iamOS *IAMObjectStore) unlock() {
|
|
|
|
iamOS.Unlock()
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
|
|
|
|
2021-11-03 22:47:49 -04:00
|
|
|
func (iamOS *IAMObjectStore) getUsersSysType() UsersSysType {
|
|
|
|
return iamOS.usersSysType
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
|
|
|
|
2021-04-22 11:45:30 -04:00
|
|
|
func (iamOS *IAMObjectStore) saveIAMConfig(ctx context.Context, item interface{}, objPath string, opts ...options) error {
|
2022-01-02 12:15:06 -05:00
|
|
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
2019-08-08 18:10:04 -04:00
|
|
|
data, err := json.Marshal(item)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-04-22 11:45:30 -04:00
|
|
|
if GlobalKMS != nil {
|
|
|
|
data, err = config.EncryptBytes(GlobalKMS, data, kms.Context{
|
|
|
|
minioMetaBucket: path.Join(minioMetaBucket, objPath),
|
|
|
|
})
|
2019-11-01 18:53:16 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2021-04-22 11:45:30 -04:00
|
|
|
return saveConfig(ctx, iamOS.objAPI, objPath, data)
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
|
|
|
|
2023-04-21 16:56:08 -04:00
|
|
|
func decryptData(data []byte, objPath string) ([]byte, error) {
|
|
|
|
if utf8.Valid(data) {
|
|
|
|
return data, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
pdata, err := madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(data))
|
|
|
|
if err == nil {
|
|
|
|
return pdata, nil
|
|
|
|
}
|
|
|
|
if GlobalKMS != nil {
|
|
|
|
pdata, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
|
|
|
|
minioMetaBucket: path.Join(minioMetaBucket, objPath),
|
|
|
|
})
|
|
|
|
if err == nil {
|
|
|
|
return pdata, nil
|
|
|
|
}
|
|
|
|
pdata, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
|
|
|
|
minioMetaBucket: objPath,
|
|
|
|
})
|
|
|
|
if err == nil {
|
|
|
|
return pdata, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-12-11 12:03:39 -05:00
|
|
|
func (iamOS *IAMObjectStore) loadIAMConfigBytesWithMetadata(ctx context.Context, objPath string) ([]byte, ObjectInfo, error) {
|
2022-10-25 15:36:57 -04:00
|
|
|
data, meta, err := readConfigWithMetadata(ctx, iamOS.objAPI, objPath, ObjectOptions{})
|
2019-08-08 18:10:04 -04:00
|
|
|
if err != nil {
|
2021-12-11 12:03:39 -05:00
|
|
|
return nil, meta, err
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
2023-04-21 16:56:08 -04:00
|
|
|
data, err = decryptData(data, objPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, meta, err
|
2019-11-01 18:53:16 -04:00
|
|
|
}
|
2021-12-11 12:03:39 -05:00
|
|
|
return data, meta, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (iamOS *IAMObjectStore) loadIAMConfig(ctx context.Context, item interface{}, objPath string) error {
|
|
|
|
data, _, err := iamOS.loadIAMConfigBytesWithMetadata(ctx, objPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-01-02 12:15:06 -05:00
|
|
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
2019-08-08 18:10:04 -04:00
|
|
|
return json.Unmarshal(data, item)
|
|
|
|
}
|
|
|
|
|
2020-10-19 12:54:40 -04:00
|
|
|
func (iamOS *IAMObjectStore) deleteIAMConfig(ctx context.Context, path string) error {
|
|
|
|
return deleteConfig(ctx, iamOS.objAPI, path)
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
|
|
|
|
2024-03-01 19:21:34 -05:00
|
|
|
func (iamOS *IAMObjectStore) loadPolicyDocWithRetry(ctx context.Context, policy string, m map[string]PolicyDoc, retries int) error {
|
|
|
|
for {
|
|
|
|
retry:
|
|
|
|
data, objInfo, err := iamOS.loadIAMConfigBytesWithMetadata(ctx, getPolicyDocPath(policy))
|
|
|
|
if err != nil {
|
|
|
|
if err == errConfigNotFound {
|
|
|
|
return errNoSuchPolicy
|
|
|
|
}
|
|
|
|
retries--
|
|
|
|
if retries <= 0 {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
goto retry
|
|
|
|
}
|
|
|
|
|
|
|
|
var p PolicyDoc
|
|
|
|
err = p.parseJSON(data)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if p.Version == 0 {
|
|
|
|
// This means that policy was in the old version (without any
|
|
|
|
// timestamp info). We fetch the mod time of the file and save
|
|
|
|
// that as create and update date.
|
|
|
|
p.CreateDate = objInfo.ModTime
|
|
|
|
p.UpdateDate = objInfo.ModTime
|
|
|
|
}
|
|
|
|
|
|
|
|
m[policy] = p
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-11 12:03:39 -05:00
|
|
|
func (iamOS *IAMObjectStore) loadPolicyDoc(ctx context.Context, policy string, m map[string]PolicyDoc) error {
|
|
|
|
data, objInfo, err := iamOS.loadIAMConfigBytesWithMetadata(ctx, getPolicyDocPath(policy))
|
2019-08-08 18:10:04 -04:00
|
|
|
if err != nil {
|
2019-10-29 22:50:26 -04:00
|
|
|
if err == errConfigNotFound {
|
|
|
|
return errNoSuchPolicy
|
|
|
|
}
|
2019-08-08 18:10:04 -04:00
|
|
|
return err
|
|
|
|
}
|
2021-12-11 12:03:39 -05:00
|
|
|
|
|
|
|
var p PolicyDoc
|
|
|
|
err = p.parseJSON(data)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if p.Version == 0 {
|
|
|
|
// This means that policy was in the old version (without any
|
|
|
|
// timestamp info). We fetch the mod time of the file and save
|
|
|
|
// that as create and update date.
|
|
|
|
p.CreateDate = objInfo.ModTime
|
|
|
|
p.UpdateDate = objInfo.ModTime
|
|
|
|
}
|
|
|
|
|
2019-08-08 18:10:04 -04:00
|
|
|
m[policy] = p
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-12-11 12:03:39 -05:00
|
|
|
func (iamOS *IAMObjectStore) loadPolicyDocs(ctx context.Context, m map[string]PolicyDoc) error {
|
2022-08-04 16:20:43 -04:00
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
defer cancel()
|
2020-12-19 12:36:37 -05:00
|
|
|
for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPoliciesPrefix) {
|
2019-08-08 18:10:04 -04:00
|
|
|
if item.Err != nil {
|
|
|
|
return item.Err
|
|
|
|
}
|
|
|
|
|
2020-12-19 12:36:37 -05:00
|
|
|
policyName := path.Dir(item.Item)
|
2022-11-14 10:15:46 -05:00
|
|
|
if err := iamOS.loadPolicyDoc(ctx, policyName, m); err != nil && !errors.Is(err, errNoSuchPolicy) {
|
2019-08-08 18:10:04 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-07-01 16:19:13 -04:00
|
|
|
func (iamOS *IAMObjectStore) loadUser(ctx context.Context, user string, userType IAMUserType, m map[string]UserIdentity) error {
|
2019-08-08 18:10:04 -04:00
|
|
|
var u UserIdentity
|
2020-10-19 12:54:40 -04:00
|
|
|
err := iamOS.loadIAMConfig(ctx, &u, getUserIdentityPath(user, userType))
|
2019-08-08 18:10:04 -04:00
|
|
|
if err != nil {
|
2019-10-29 22:50:26 -04:00
|
|
|
if err == errConfigNotFound {
|
|
|
|
return errNoSuchUser
|
|
|
|
}
|
2019-08-08 18:10:04 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if u.Credentials.IsExpired() {
|
|
|
|
// Delete expired identity - ignoring errors here.
|
2020-10-19 12:54:40 -04:00
|
|
|
iamOS.deleteIAMConfig(ctx, getUserIdentityPath(user, userType))
|
|
|
|
iamOS.deleteIAMConfig(ctx, getMappedPolicyPath(user, userType, false))
|
2019-08-08 18:10:04 -04:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if u.Credentials.AccessKey == "" {
|
|
|
|
u.Credentials.AccessKey = user
|
|
|
|
}
|
2020-08-05 16:08:40 -04:00
|
|
|
|
2023-02-27 13:10:22 -05:00
|
|
|
if u.Credentials.SessionToken != "" {
|
|
|
|
jwtClaims, err := extractJWTClaims(u)
|
|
|
|
if err != nil {
|
2023-03-15 11:07:42 -04:00
|
|
|
if u.Credentials.IsTemp() {
|
|
|
|
// We should delete such that the client can re-request
|
|
|
|
// for the expiring credentials.
|
|
|
|
iamOS.deleteIAMConfig(ctx, getUserIdentityPath(user, userType))
|
|
|
|
iamOS.deleteIAMConfig(ctx, getMappedPolicyPath(user, userType, false))
|
|
|
|
}
|
2024-06-13 18:26:38 -04:00
|
|
|
return nil
|
2023-03-15 11:07:42 -04:00
|
|
|
|
2023-02-27 13:10:22 -05:00
|
|
|
}
|
|
|
|
u.Credentials.Claims = jwtClaims.Map()
|
|
|
|
}
|
|
|
|
|
2023-05-17 20:05:36 -04:00
|
|
|
if u.Credentials.Description == "" {
|
|
|
|
u.Credentials.Description = u.Credentials.Comment
|
|
|
|
}
|
|
|
|
|
2022-07-01 16:19:13 -04:00
|
|
|
m[user] = u
|
2019-08-08 18:10:04 -04:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-07-01 16:19:13 -04:00
|
|
|
func (iamOS *IAMObjectStore) loadUsers(ctx context.Context, userType IAMUserType, m map[string]UserIdentity) error {
|
2020-03-17 13:36:13 -04:00
|
|
|
var basePrefix string
|
|
|
|
switch userType {
|
2021-07-09 14:17:21 -04:00
|
|
|
case svcUser:
|
2020-03-17 13:36:13 -04:00
|
|
|
basePrefix = iamConfigServiceAccountsPrefix
|
|
|
|
case stsUser:
|
2019-08-08 18:10:04 -04:00
|
|
|
basePrefix = iamConfigSTSPrefix
|
2020-03-17 13:36:13 -04:00
|
|
|
default:
|
|
|
|
basePrefix = iamConfigUsersPrefix
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
2020-03-17 13:36:13 -04:00
|
|
|
|
2022-08-04 16:20:43 -04:00
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
defer cancel()
|
2020-12-19 12:36:37 -05:00
|
|
|
for item := range listIAMConfigItems(ctx, iamOS.objAPI, basePrefix) {
|
2019-08-08 18:10:04 -04:00
|
|
|
if item.Err != nil {
|
|
|
|
return item.Err
|
|
|
|
}
|
|
|
|
|
2020-12-19 12:36:37 -05:00
|
|
|
userName := path.Dir(item.Item)
|
2020-10-19 12:54:40 -04:00
|
|
|
if err := iamOS.loadUser(ctx, userName, userType, m); err != nil && err != errNoSuchUser {
|
2019-08-08 18:10:04 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-19 12:54:40 -04:00
|
|
|
func (iamOS *IAMObjectStore) loadGroup(ctx context.Context, group string, m map[string]GroupInfo) error {
|
2019-08-08 18:10:04 -04:00
|
|
|
var g GroupInfo
|
2020-10-19 12:54:40 -04:00
|
|
|
err := iamOS.loadIAMConfig(ctx, &g, getGroupInfoPath(group))
|
2019-08-08 18:10:04 -04:00
|
|
|
if err != nil {
|
2019-10-29 22:50:26 -04:00
|
|
|
if err == errConfigNotFound {
|
|
|
|
return errNoSuchGroup
|
|
|
|
}
|
2019-08-08 18:10:04 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
m[group] = g
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-04-07 17:26:39 -04:00
|
|
|
func (iamOS *IAMObjectStore) loadGroups(ctx context.Context, m map[string]GroupInfo) error {
|
2022-08-04 16:20:43 -04:00
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
defer cancel()
|
2020-12-19 12:36:37 -05:00
|
|
|
for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigGroupsPrefix) {
|
2019-08-08 18:10:04 -04:00
|
|
|
if item.Err != nil {
|
|
|
|
return item.Err
|
|
|
|
}
|
|
|
|
|
2020-12-19 12:36:37 -05:00
|
|
|
group := path.Dir(item.Item)
|
2020-10-19 12:54:40 -04:00
|
|
|
if err := iamOS.loadGroup(ctx, group, m); err != nil && err != errNoSuchGroup {
|
2019-08-08 18:10:04 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-03-26 14:12:57 -04:00
|
|
|
func (iamOS *IAMObjectStore) loadMappedPolicyWithRetry(ctx context.Context, name string, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy], retries int) error {
|
2024-03-01 19:21:34 -05:00
|
|
|
for {
|
|
|
|
retry:
|
|
|
|
var p MappedPolicy
|
|
|
|
err := iamOS.loadIAMConfig(ctx, &p, getMappedPolicyPath(name, userType, isGroup))
|
|
|
|
if err != nil {
|
|
|
|
if err == errConfigNotFound {
|
|
|
|
return errNoSuchPolicy
|
|
|
|
}
|
|
|
|
retries--
|
|
|
|
if retries <= 0 {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
goto retry
|
|
|
|
}
|
|
|
|
|
2024-03-26 14:12:57 -04:00
|
|
|
m.Store(name, p)
|
2024-03-01 19:21:34 -05:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-26 14:12:57 -04:00
|
|
|
func (iamOS *IAMObjectStore) loadMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy]) error {
|
2019-08-08 18:10:04 -04:00
|
|
|
var p MappedPolicy
|
2020-10-19 12:54:40 -04:00
|
|
|
err := iamOS.loadIAMConfig(ctx, &p, getMappedPolicyPath(name, userType, isGroup))
|
2019-08-08 18:10:04 -04:00
|
|
|
if err != nil {
|
2019-10-29 22:50:26 -04:00
|
|
|
if err == errConfigNotFound {
|
|
|
|
return errNoSuchPolicy
|
|
|
|
}
|
2019-08-08 18:10:04 -04:00
|
|
|
return err
|
|
|
|
}
|
2024-03-01 19:21:34 -05:00
|
|
|
|
2024-03-26 14:12:57 -04:00
|
|
|
m.Store(name, p)
|
2019-08-08 18:10:04 -04:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-03-26 14:12:57 -04:00
|
|
|
func (iamOS *IAMObjectStore) loadMappedPolicies(ctx context.Context, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy]) error {
|
2019-08-08 18:10:04 -04:00
|
|
|
var basePath string
|
2020-03-17 13:36:13 -04:00
|
|
|
if isGroup {
|
2019-08-08 18:10:04 -04:00
|
|
|
basePath = iamConfigPolicyDBGroupsPrefix
|
2020-03-17 13:36:13 -04:00
|
|
|
} else {
|
|
|
|
switch userType {
|
2021-07-09 14:17:21 -04:00
|
|
|
case svcUser:
|
2020-03-17 13:36:13 -04:00
|
|
|
basePath = iamConfigPolicyDBServiceAccountsPrefix
|
|
|
|
case stsUser:
|
|
|
|
basePath = iamConfigPolicyDBSTSUsersPrefix
|
|
|
|
default:
|
|
|
|
basePath = iamConfigPolicyDBUsersPrefix
|
|
|
|
}
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
2022-08-04 16:20:43 -04:00
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
defer cancel()
|
2020-12-19 12:36:37 -05:00
|
|
|
for item := range listIAMConfigItems(ctx, iamOS.objAPI, basePath) {
|
2019-08-08 18:10:04 -04:00
|
|
|
if item.Err != nil {
|
|
|
|
return item.Err
|
|
|
|
}
|
|
|
|
|
|
|
|
policyFile := item.Item
|
|
|
|
userOrGroupName := strings.TrimSuffix(policyFile, ".json")
|
2022-11-14 10:15:46 -05:00
|
|
|
if err := iamOS.loadMappedPolicy(ctx, userOrGroupName, userType, isGroup, m); err != nil && !errors.Is(err, errNoSuchPolicy) {
|
2019-08-08 18:10:04 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-03-27 21:48:01 -04:00
|
|
|
var (
|
2024-03-28 19:43:50 -04:00
|
|
|
usersListKey = "users/"
|
|
|
|
svcAccListKey = "service-accounts/"
|
|
|
|
groupsListKey = "groups/"
|
|
|
|
policiesListKey = "policies/"
|
|
|
|
stsListKey = "sts/"
|
2024-04-05 17:26:41 -04:00
|
|
|
policyDBPrefix = "policydb/"
|
2024-03-28 19:43:50 -04:00
|
|
|
policyDBUsersListKey = "policydb/users/"
|
|
|
|
policyDBSTSUsersListKey = "policydb/sts-users/"
|
|
|
|
policyDBGroupsListKey = "policydb/groups/"
|
2022-03-27 21:48:01 -04:00
|
|
|
)
|
|
|
|
|
2024-03-28 19:43:50 -04:00
|
|
|
// splitPath splits a path into a top-level directory and a child item. The
|
|
|
|
// parent directory retains the trailing slash.
|
2024-04-05 17:26:41 -04:00
|
|
|
func splitPath(s string, lastIndex bool) (string, string) {
|
|
|
|
var i int
|
|
|
|
if lastIndex {
|
|
|
|
i = strings.LastIndex(s, "/")
|
|
|
|
} else {
|
|
|
|
i = strings.Index(s, "/")
|
|
|
|
}
|
2024-03-28 19:43:50 -04:00
|
|
|
if i == -1 {
|
|
|
|
return s, ""
|
|
|
|
}
|
|
|
|
// Include the trailing slash in the parent directory.
|
|
|
|
return s[:i+1], s[i+1:]
|
|
|
|
}
|
|
|
|
|
2024-06-10 14:45:50 -04:00
|
|
|
func (iamOS *IAMObjectStore) listAllIAMConfigItems(ctx context.Context) (res map[string][]string, err error) {
|
|
|
|
res = make(map[string][]string)
|
2022-08-04 16:20:43 -04:00
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
defer cancel()
|
2024-03-28 19:43:50 -04:00
|
|
|
for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPrefix+SlashSeparator) {
|
|
|
|
if item.Err != nil {
|
|
|
|
return nil, item.Err
|
2022-03-27 21:48:01 -04:00
|
|
|
}
|
2024-03-28 19:43:50 -04:00
|
|
|
|
2024-04-05 17:26:41 -04:00
|
|
|
lastIndex := strings.HasPrefix(item.Item, policyDBPrefix)
|
|
|
|
listKey, trimmedItem := splitPath(item.Item, lastIndex)
|
2024-03-28 19:43:50 -04:00
|
|
|
if listKey == iamFormatFile {
|
|
|
|
continue
|
2022-03-27 21:48:01 -04:00
|
|
|
}
|
2024-03-28 19:43:50 -04:00
|
|
|
|
|
|
|
res[listKey] = append(res[listKey], trimmedItem)
|
2022-03-27 21:48:01 -04:00
|
|
|
}
|
2024-03-28 19:43:50 -04:00
|
|
|
|
2024-06-10 14:45:50 -04:00
|
|
|
return res, nil
|
2023-09-14 18:25:17 -04:00
|
|
|
}
|
|
|
|
|
2024-06-25 13:33:10 -04:00
|
|
|
const (
|
|
|
|
maxIAMLoadOpTime = 5 * time.Second
|
|
|
|
)
|
|
|
|
|
2022-03-27 21:48:01 -04:00
|
|
|
// Assumes cache is locked by caller.
|
2024-06-24 22:30:28 -04:00
|
|
|
func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iamCache, firstTime bool) error {
|
|
|
|
bootstrapTraceMsgFirstTime := func(s string) {
|
|
|
|
if firstTime {
|
|
|
|
bootstrapTraceMsg(s)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-02 17:32:54 -05:00
|
|
|
if iamOS.objAPI == nil {
|
|
|
|
return errServerNotInitialized
|
|
|
|
}
|
2023-08-23 06:07:06 -04:00
|
|
|
|
2024-06-24 22:30:28 -04:00
|
|
|
bootstrapTraceMsgFirstTime("loading all IAM items")
|
2023-08-23 06:07:06 -04:00
|
|
|
|
2024-06-27 20:03:07 -04:00
|
|
|
setDefaultCannedPolicies(cache.iamPolicyDocsMap)
|
|
|
|
|
2024-06-25 13:33:10 -04:00
|
|
|
listStartTime := UTCNow()
|
2022-03-27 21:48:01 -04:00
|
|
|
listedConfigItems, err := iamOS.listAllIAMConfigItems(ctx)
|
|
|
|
if err != nil {
|
2023-07-14 23:17:14 -04:00
|
|
|
return fmt.Errorf("unable to list IAM data: %w", err)
|
2022-03-27 21:48:01 -04:00
|
|
|
}
|
2024-06-25 13:33:10 -04:00
|
|
|
if took := time.Since(listStartTime); took > maxIAMLoadOpTime {
|
|
|
|
var s strings.Builder
|
|
|
|
for k, v := range listedConfigItems {
|
|
|
|
s.WriteString(fmt.Sprintf(" %s: %d items\n", k, len(v)))
|
|
|
|
}
|
|
|
|
logger.Info("listAllIAMConfigItems took %.2fs with contents:\n%s", took.Seconds(), s.String())
|
|
|
|
}
|
2022-03-27 21:48:01 -04:00
|
|
|
|
|
|
|
// Loads things in the same order as `LoadIAMCache()`
|
|
|
|
|
2024-06-24 22:30:28 -04:00
|
|
|
bootstrapTraceMsgFirstTime("loading policy documents")
|
2022-12-21 18:52:29 -05:00
|
|
|
|
2024-06-25 13:33:10 -04:00
|
|
|
policyLoadStartTime := UTCNow()
|
2022-03-27 21:48:01 -04:00
|
|
|
policiesList := listedConfigItems[policiesListKey]
|
|
|
|
for _, item := range policiesList {
|
|
|
|
policyName := path.Dir(item)
|
2022-11-14 10:15:46 -05:00
|
|
|
if err := iamOS.loadPolicyDoc(ctx, policyName, cache.iamPolicyDocsMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
|
2023-07-14 23:17:14 -04:00
|
|
|
return fmt.Errorf("unable to load the policy doc `%s`: %w", policyName, err)
|
2022-03-27 21:48:01 -04:00
|
|
|
}
|
|
|
|
}
|
2024-06-25 13:33:10 -04:00
|
|
|
if took := time.Since(policyLoadStartTime); took > maxIAMLoadOpTime {
|
|
|
|
logger.Info("Policy docs load took %.2fs (for %d items)", took.Seconds(), len(policiesList))
|
|
|
|
}
|
2022-03-27 21:48:01 -04:00
|
|
|
|
|
|
|
if iamOS.usersSysType == MinIOUsersSysType {
|
2024-06-24 22:30:28 -04:00
|
|
|
bootstrapTraceMsgFirstTime("loading regular IAM users")
|
2024-06-25 13:33:10 -04:00
|
|
|
regUsersLoadStartTime := UTCNow()
|
2022-03-27 21:48:01 -04:00
|
|
|
regUsersList := listedConfigItems[usersListKey]
|
|
|
|
for _, item := range regUsersList {
|
|
|
|
userName := path.Dir(item)
|
|
|
|
if err := iamOS.loadUser(ctx, userName, regUser, cache.iamUsersMap); err != nil && err != errNoSuchUser {
|
2024-07-14 14:12:07 -04:00
|
|
|
return fmt.Errorf("unable to load the user: %w", err)
|
2022-03-27 21:48:01 -04:00
|
|
|
}
|
|
|
|
}
|
2024-06-25 13:33:10 -04:00
|
|
|
if took := time.Since(regUsersLoadStartTime); took > maxIAMLoadOpTime {
|
|
|
|
actualLoaded := len(cache.iamUsersMap)
|
|
|
|
logger.Info("Reg. users load took %.2fs (for %d items with %d expired items)", took.Seconds(),
|
|
|
|
len(regUsersList), len(regUsersList)-actualLoaded)
|
|
|
|
}
|
2022-03-27 21:48:01 -04:00
|
|
|
|
2024-06-24 22:30:28 -04:00
|
|
|
bootstrapTraceMsgFirstTime("loading regular IAM groups")
|
2024-06-25 13:33:10 -04:00
|
|
|
groupsLoadStartTime := UTCNow()
|
2022-03-27 21:48:01 -04:00
|
|
|
groupsList := listedConfigItems[groupsListKey]
|
|
|
|
for _, item := range groupsList {
|
|
|
|
group := path.Dir(item)
|
|
|
|
if err := iamOS.loadGroup(ctx, group, cache.iamGroupsMap); err != nil && err != errNoSuchGroup {
|
2024-07-14 14:12:07 -04:00
|
|
|
return fmt.Errorf("unable to load the group: %w", err)
|
2022-03-27 21:48:01 -04:00
|
|
|
}
|
|
|
|
}
|
2024-06-25 13:33:10 -04:00
|
|
|
if took := time.Since(groupsLoadStartTime); took > maxIAMLoadOpTime {
|
|
|
|
logger.Info("Groups load took %.2fs (for %d items)", took.Seconds(), len(groupsList))
|
|
|
|
}
|
2022-03-27 21:48:01 -04:00
|
|
|
}
|
|
|
|
|
2024-06-24 22:30:28 -04:00
|
|
|
bootstrapTraceMsgFirstTime("loading user policy mapping")
|
2024-06-25 13:33:10 -04:00
|
|
|
userPolicyMappingLoadStartTime := UTCNow()
|
2022-03-27 21:48:01 -04:00
|
|
|
userPolicyMappingsList := listedConfigItems[policyDBUsersListKey]
|
|
|
|
for _, item := range userPolicyMappingsList {
|
|
|
|
userName := strings.TrimSuffix(item, ".json")
|
2022-11-14 10:15:46 -05:00
|
|
|
if err := iamOS.loadMappedPolicy(ctx, userName, regUser, false, cache.iamUserPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
|
2024-07-14 14:12:07 -04:00
|
|
|
return fmt.Errorf("unable to load the policy mapping for the user: %w", err)
|
2022-03-27 21:48:01 -04:00
|
|
|
}
|
|
|
|
}
|
2024-06-25 13:33:10 -04:00
|
|
|
if took := time.Since(userPolicyMappingLoadStartTime); took > maxIAMLoadOpTime {
|
|
|
|
logger.Info("User policy mappings load took %.2fs (for %d items)", took.Seconds(), len(userPolicyMappingsList))
|
|
|
|
}
|
2022-03-27 21:48:01 -04:00
|
|
|
|
2024-06-24 22:30:28 -04:00
|
|
|
bootstrapTraceMsgFirstTime("loading group policy mapping")
|
2024-06-25 13:33:10 -04:00
|
|
|
groupPolicyMappingLoadStartTime := UTCNow()
|
2022-03-27 21:48:01 -04:00
|
|
|
groupPolicyMappingsList := listedConfigItems[policyDBGroupsListKey]
|
|
|
|
for _, item := range groupPolicyMappingsList {
|
|
|
|
groupName := strings.TrimSuffix(item, ".json")
|
2022-11-14 10:15:46 -05:00
|
|
|
if err := iamOS.loadMappedPolicy(ctx, groupName, regUser, true, cache.iamGroupPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
|
2024-07-14 14:12:07 -04:00
|
|
|
return fmt.Errorf("unable to load the policy mapping for the group: %w", err)
|
2022-03-27 21:48:01 -04:00
|
|
|
}
|
|
|
|
}
|
2024-06-25 13:33:10 -04:00
|
|
|
if took := time.Since(groupPolicyMappingLoadStartTime); took > maxIAMLoadOpTime {
|
|
|
|
logger.Info("Group policy mappings load took %.2fs (for %d items)", took.Seconds(), len(groupPolicyMappingsList))
|
|
|
|
}
|
2022-03-27 21:48:01 -04:00
|
|
|
|
2024-06-24 22:30:28 -04:00
|
|
|
bootstrapTraceMsgFirstTime("loading service accounts")
|
2024-06-25 13:33:10 -04:00
|
|
|
svcAccLoadStartTime := UTCNow()
|
2022-03-27 21:48:01 -04:00
|
|
|
svcAccList := listedConfigItems[svcAccListKey]
|
2023-09-19 20:57:42 -04:00
|
|
|
svcUsersMap := make(map[string]UserIdentity, len(svcAccList))
|
2022-03-27 21:48:01 -04:00
|
|
|
for _, item := range svcAccList {
|
|
|
|
userName := path.Dir(item)
|
2023-09-19 20:57:42 -04:00
|
|
|
if err := iamOS.loadUser(ctx, userName, svcUser, svcUsersMap); err != nil && err != errNoSuchUser {
|
2024-07-14 14:12:07 -04:00
|
|
|
return fmt.Errorf("unable to load the service account: %w", err)
|
2022-03-27 21:48:01 -04:00
|
|
|
}
|
|
|
|
}
|
2024-06-25 13:33:10 -04:00
|
|
|
if took := time.Since(svcAccLoadStartTime); took > maxIAMLoadOpTime {
|
|
|
|
logger.Info("Service accounts load took %.2fs (for %d items with %d expired items)", took.Seconds(),
|
|
|
|
len(svcAccList), len(svcAccList)-len(svcUsersMap))
|
|
|
|
}
|
|
|
|
|
|
|
|
bootstrapTraceMsg("loading STS account policy mapping")
|
|
|
|
stsPolicyMappingLoadStartTime := UTCNow()
|
|
|
|
var stsPolicyMappingsCount int
|
2023-09-19 20:57:42 -04:00
|
|
|
for _, svcAcc := range svcUsersMap {
|
|
|
|
svcParent := svcAcc.Credentials.ParentUser
|
|
|
|
if _, ok := cache.iamUsersMap[svcParent]; !ok {
|
2024-06-25 13:33:10 -04:00
|
|
|
stsPolicyMappingsCount++
|
2023-09-19 20:57:42 -04:00
|
|
|
// If a service account's parent user is not in iamUsersMap, the
|
|
|
|
// parent is an STS account. Such accounts may have a policy mapped
|
|
|
|
// on the parent user, so we load them. This is not needed for the
|
|
|
|
// initial server startup, however, it is needed for the case where
|
|
|
|
// the STS account's policy mapping (for example in LDAP mode) may
|
|
|
|
// be changed and the user's policy mapping in memory is stale
|
|
|
|
// (because the policy change notification was missed by the current
|
|
|
|
// server).
|
|
|
|
//
|
|
|
|
// The "policy not found" error is ignored because the STS account may
|
|
|
|
// not have a policy mapped via its parent (for e.g. in
|
|
|
|
// OIDC/AssumeRoleWithCustomToken/AssumeRoleWithCertificate).
|
|
|
|
err := iamOS.loadMappedPolicy(ctx, svcParent, stsUser, false, cache.iamSTSPolicyMap)
|
|
|
|
if err != nil && !errors.Is(err, errNoSuchPolicy) {
|
2024-07-14 14:12:07 -04:00
|
|
|
return fmt.Errorf("unable to load the policy mapping for the STS user: %w", err)
|
2023-09-19 20:57:42 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-06-25 13:33:10 -04:00
|
|
|
if took := time.Since(stsPolicyMappingLoadStartTime); took > maxIAMLoadOpTime {
|
|
|
|
logger.Info("STS policy mappings load took %.2fs (for %d items)", took.Seconds(), stsPolicyMappingsCount)
|
|
|
|
}
|
|
|
|
|
2023-09-19 20:57:42 -04:00
|
|
|
// Copy svcUsersMap to cache.iamUsersMap
|
|
|
|
for k, v := range svcUsersMap {
|
|
|
|
cache.iamUsersMap[k] = v
|
|
|
|
}
|
2022-03-27 21:48:01 -04:00
|
|
|
|
|
|
|
cache.buildUserGroupMemberships()
|
2024-06-10 14:45:50 -04:00
|
|
|
|
|
|
|
purgeStart := time.Now()
|
|
|
|
|
|
|
|
// Purge expired STS credentials.
|
|
|
|
|
|
|
|
// Scan STS users on disk and purge expired ones.
|
|
|
|
stsAccountsFromStore := map[string]UserIdentity{}
|
|
|
|
stsAccPoliciesFromStore := xsync.NewMapOf[string, MappedPolicy]()
|
|
|
|
for _, item := range listedConfigItems[stsListKey] {
|
|
|
|
userName := path.Dir(item)
|
|
|
|
// loadUser() will delete expired user during the load.
|
|
|
|
err := iamOS.loadUser(ctx, userName, stsUser, stsAccountsFromStore)
|
|
|
|
if err != nil && !errors.Is(err, errNoSuchUser) {
|
|
|
|
return fmt.Errorf("unable to load user during STS purge: %w (%s)", err, item)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
// Loading the STS policy mappings from disk ensures that stale entries
|
|
|
|
// (removed during loadUser() in the loop above) are removed from memory.
|
|
|
|
for _, item := range listedConfigItems[policyDBSTSUsersListKey] {
|
|
|
|
stsName := strings.TrimSuffix(item, ".json")
|
|
|
|
err := iamOS.loadMappedPolicy(ctx, stsName, stsUser, false, stsAccPoliciesFromStore)
|
|
|
|
if err != nil && !errors.Is(err, errNoSuchPolicy) {
|
|
|
|
return fmt.Errorf("unable to load policies during STS purge: %w (%s)", err, item)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store the newly populated map in the iam cache. This takes care of
|
|
|
|
// removing stale entries from the existing map.
|
|
|
|
cache.iamSTSAccountsMap = stsAccountsFromStore
|
|
|
|
cache.iamSTSPolicyMap = stsAccPoliciesFromStore
|
|
|
|
|
2022-03-27 21:48:01 -04:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-12-11 12:03:39 -05:00
|
|
|
func (iamOS *IAMObjectStore) savePolicyDoc(ctx context.Context, policyName string, p PolicyDoc) error {
|
2020-10-19 12:54:40 -04:00
|
|
|
return iamOS.saveIAMConfig(ctx, &p, getPolicyDocPath(policyName))
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
|
|
|
|
2020-11-04 16:06:05 -05:00
|
|
|
func (iamOS *IAMObjectStore) saveMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, mp MappedPolicy, opts ...options) error {
|
|
|
|
return iamOS.saveIAMConfig(ctx, mp, getMappedPolicyPath(name, userType, isGroup), opts...)
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
|
|
|
|
2020-11-04 16:06:05 -05:00
|
|
|
func (iamOS *IAMObjectStore) saveUserIdentity(ctx context.Context, name string, userType IAMUserType, u UserIdentity, opts ...options) error {
|
|
|
|
return iamOS.saveIAMConfig(ctx, u, getUserIdentityPath(name, userType), opts...)
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
|
|
|
|
2020-10-19 12:54:40 -04:00
|
|
|
func (iamOS *IAMObjectStore) saveGroupInfo(ctx context.Context, name string, gi GroupInfo) error {
|
|
|
|
return iamOS.saveIAMConfig(ctx, gi, getGroupInfoPath(name))
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
|
|
|
|
2020-10-19 12:54:40 -04:00
|
|
|
func (iamOS *IAMObjectStore) deletePolicyDoc(ctx context.Context, name string) error {
|
|
|
|
err := iamOS.deleteIAMConfig(ctx, getPolicyDocPath(name))
|
2019-10-29 22:50:26 -04:00
|
|
|
if err == errConfigNotFound {
|
|
|
|
err = errNoSuchPolicy
|
|
|
|
}
|
|
|
|
return err
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
|
|
|
|
2020-10-19 12:54:40 -04:00
|
|
|
func (iamOS *IAMObjectStore) deleteMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool) error {
|
|
|
|
err := iamOS.deleteIAMConfig(ctx, getMappedPolicyPath(name, userType, isGroup))
|
2019-10-29 22:50:26 -04:00
|
|
|
if err == errConfigNotFound {
|
|
|
|
err = errNoSuchPolicy
|
|
|
|
}
|
|
|
|
return err
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
|
|
|
|
2020-10-19 12:54:40 -04:00
|
|
|
func (iamOS *IAMObjectStore) deleteUserIdentity(ctx context.Context, name string, userType IAMUserType) error {
|
|
|
|
err := iamOS.deleteIAMConfig(ctx, getUserIdentityPath(name, userType))
|
2019-10-29 22:50:26 -04:00
|
|
|
if err == errConfigNotFound {
|
|
|
|
err = errNoSuchUser
|
|
|
|
}
|
|
|
|
return err
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
|
|
|
|
2020-10-19 12:54:40 -04:00
|
|
|
func (iamOS *IAMObjectStore) deleteGroupInfo(ctx context.Context, name string) error {
|
|
|
|
err := iamOS.deleteIAMConfig(ctx, getGroupInfoPath(name))
|
2019-10-29 22:50:26 -04:00
|
|
|
if err == errConfigNotFound {
|
|
|
|
err = errNoSuchGroup
|
|
|
|
}
|
|
|
|
return err
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
|
|
|
|
2024-03-28 19:43:50 -04:00
|
|
|
// Lists objects in the minioMetaBucket at the given path prefix. All returned
|
|
|
|
// items have the pathPrefix removed from their names.
|
2024-05-06 16:27:52 -04:00
|
|
|
func listIAMConfigItems(ctx context.Context, objAPI ObjectLayer, pathPrefix string) <-chan itemOrErr[string] {
|
|
|
|
ch := make(chan itemOrErr[string])
|
2020-04-07 17:26:39 -04:00
|
|
|
|
2019-08-08 18:10:04 -04:00
|
|
|
go func() {
|
2024-01-28 13:04:17 -05:00
|
|
|
defer xioutil.SafeClose(ch)
|
2020-02-13 09:36:23 -05:00
|
|
|
|
2020-12-19 12:36:37 -05:00
|
|
|
// Allocate new results channel to receive ObjectInfo.
|
2024-05-06 16:27:52 -04:00
|
|
|
objInfoCh := make(chan itemOrErr[ObjectInfo])
|
2020-02-13 09:36:23 -05:00
|
|
|
|
2023-11-27 20:20:04 -05:00
|
|
|
if err := objAPI.Walk(ctx, minioMetaBucket, pathPrefix, objInfoCh, WalkOptions{}); err != nil {
|
2020-12-19 12:36:37 -05:00
|
|
|
select {
|
2024-05-06 16:27:52 -04:00
|
|
|
case ch <- itemOrErr[string]{Err: err}:
|
2020-12-19 12:36:37 -05:00
|
|
|
case <-ctx.Done():
|
2019-08-08 18:10:04 -04:00
|
|
|
}
|
2020-12-19 12:36:37 -05:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for obj := range objInfoCh {
|
2024-05-06 16:27:52 -04:00
|
|
|
if obj.Err != nil {
|
|
|
|
select {
|
|
|
|
case ch <- itemOrErr[string]{Err: obj.Err}:
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
item := strings.TrimPrefix(obj.Item.Name, pathPrefix)
|
2020-12-19 12:36:37 -05:00
|
|
|
item = strings.TrimSuffix(item, SlashSeparator)
|
|
|
|
select {
|
2024-05-06 16:27:52 -04:00
|
|
|
case ch <- itemOrErr[string]{Item: item}:
|
2020-12-19 12:36:37 -05:00
|
|
|
case <-ctx.Done():
|
2019-08-08 18:10:04 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
2020-04-07 17:26:39 -04:00
|
|
|
|
2019-08-08 18:10:04 -04:00
|
|
|
return ch
|
|
|
|
}
|