mirror of
https://github.com/pgsty/minio.git
synced 2026-09-17 07:48:27 +03:00
7b4cacc392
Signed-off-by: Feng Ruohang <rh@vonng.com>
221 lines
6.2 KiB
Go
221 lines
6.2 KiB
Go
// Copyright (c) 2026 PGSTY
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/minio/minio/internal/auth"
|
|
)
|
|
|
|
// This index is rebuilt by the existing IAM loaders and updated by successful
|
|
// storage operations. It avoids a second full IAM walk during every heal pass.
|
|
// It is an optimization of the durable records, never a reason to delete them.
|
|
// The index contains no secrets or grants.
|
|
type iamParentRevision struct {
|
|
deleted bool
|
|
before time.Time
|
|
}
|
|
|
|
type iamRevisionIndex struct {
|
|
mu sync.RWMutex
|
|
items map[string]iamRevision
|
|
parents map[string]iamParentRevision
|
|
floors map[string]time.Time
|
|
generation uint64
|
|
}
|
|
|
|
func (idx *iamRevisionIndex) observe(path string, data []byte) {
|
|
if !strings.HasPrefix(path, iamConfigPrefix+"/") {
|
|
return
|
|
}
|
|
var r iamRevision
|
|
if json.Unmarshal(data, &r) != nil {
|
|
return // The caller reports malformed data using its normal decoder.
|
|
}
|
|
r.Credentials = auth.Credentials{ParentUser: r.Credentials.ParentUser, Expiration: r.Credentials.Expiration}
|
|
idx.mu.Lock()
|
|
defer idx.mu.Unlock()
|
|
if strings.HasPrefix(path, iamConfigUsersPrefix) {
|
|
// Keep a compact name-keyed view for the authentication hot path;
|
|
// constructing a config path on every S3 request allocates needlessly.
|
|
defer func() {
|
|
name := strings.TrimSuffix(strings.TrimPrefix(path, iamConfigUsersPrefix), "/"+iamIdentityFile)
|
|
if current, ok := idx.items[path]; ok {
|
|
if idx.parents == nil {
|
|
idx.parents = make(map[string]iamParentRevision)
|
|
}
|
|
idx.parents[name] = iamParentRevision{deleted: current.Deleted, before: current.RevokedBefore}
|
|
} else {
|
|
delete(idx.parents, name)
|
|
}
|
|
}()
|
|
}
|
|
if floor, ok := idx.floors[path]; ok && r.timestamp().Before(floor) {
|
|
return
|
|
}
|
|
if previous, ok := idx.items[path]; ok {
|
|
// A concurrent read that began before a write must not roll it back.
|
|
if previous.timestamp().After(r.timestamp()) || (previous.Deleted && !r.Deleted && !r.timestamp().After(previous.timestamp())) {
|
|
return
|
|
}
|
|
if previous.RevokedBefore.After(r.RevokedBefore) {
|
|
r.RevokedBefore = previous.RevokedBefore
|
|
}
|
|
if previous.timestamp().Equal(r.timestamp()) && previous.Deleted == r.Deleted && previous.RevokedBefore.Equal(r.RevokedBefore) {
|
|
return
|
|
}
|
|
}
|
|
if r.Deleted && !r.ExpiresAt.IsZero() && UTCNow().After(r.ExpiresAt) {
|
|
if _, tracked := idx.items[path]; tracked {
|
|
delete(idx.items, path)
|
|
idx.generation++
|
|
}
|
|
delete(idx.floors, path)
|
|
return
|
|
}
|
|
if !r.Deleted && r.RevokedBefore.IsZero() {
|
|
_, tracked := idx.items[path]
|
|
_, hasFloor := idx.floors[path]
|
|
if tracked || hasFloor {
|
|
if idx.floors == nil {
|
|
idx.floors = make(map[string]time.Time)
|
|
}
|
|
idx.floors[path] = r.timestamp()
|
|
}
|
|
if tracked {
|
|
delete(idx.items, path)
|
|
idx.generation++
|
|
}
|
|
return
|
|
}
|
|
if idx.items == nil {
|
|
idx.items = make(map[string]iamRevision)
|
|
}
|
|
idx.items[path] = r
|
|
delete(idx.floors, path)
|
|
idx.generation++
|
|
}
|
|
|
|
func (idx *iamRevisionIndex) get(path string) iamRevision {
|
|
if idx == nil {
|
|
return iamRevision{}
|
|
}
|
|
idx.mu.RLock()
|
|
defer idx.mu.RUnlock()
|
|
return idx.items[path]
|
|
}
|
|
|
|
func (idx *iamRevisionIndex) snapshot() map[string]iamRevision {
|
|
idx.mu.Lock()
|
|
defer idx.mu.Unlock()
|
|
for path, r := range idx.items {
|
|
if r.Deleted && !r.ExpiresAt.IsZero() && UTCNow().After(r.ExpiresAt) {
|
|
delete(idx.items, path)
|
|
delete(idx.floors, path)
|
|
idx.generation++
|
|
}
|
|
}
|
|
return maps.Clone(idx.items)
|
|
}
|
|
|
|
func (idx *iamRevisionIndex) count() int {
|
|
idx.mu.RLock()
|
|
defer idx.mu.RUnlock()
|
|
return len(idx.items)
|
|
}
|
|
|
|
// A process-local generation plus the protocol's instance ID is sufficient
|
|
// for acknowledgements. Avoid hashing the entire index on every IAM write.
|
|
func (idx *iamRevisionIndex) digest() string {
|
|
idx.mu.RLock()
|
|
defer idx.mu.RUnlock()
|
|
return fmt.Sprintf("%x:%x", idx.generation, len(idx.items))
|
|
}
|
|
|
|
func (idx *iamRevisionIndex) forget(path string) {
|
|
idx.mu.Lock()
|
|
if _, ok := idx.items[path]; ok {
|
|
delete(idx.items, path)
|
|
idx.generation++
|
|
}
|
|
delete(idx.floors, path)
|
|
if strings.HasPrefix(path, iamConfigUsersPrefix) {
|
|
delete(idx.parents, strings.TrimSuffix(strings.TrimPrefix(path, iamConfigUsersPrefix), "/"+iamIdentityFile))
|
|
}
|
|
idx.mu.Unlock()
|
|
}
|
|
|
|
func (c *iamCache) userRevocation(user string) iamRevision {
|
|
r := c.revisions.parentRevision(user)
|
|
if u, ok := c.iamUsersMap[user]; ok && u.RevokedBefore.After(r.RevokedBefore) {
|
|
r.RevokedBefore = u.RevokedBefore
|
|
}
|
|
return r
|
|
}
|
|
|
|
func (c *iamCache) groupMemberAllowed(member string, grantedAt, groupBoundary time.Time) bool {
|
|
r := c.userRevocation(member)
|
|
return !r.Deleted && (r.RevokedBefore.IsZero() || grantedAt.After(r.RevokedBefore)) && (groupBoundary.IsZero() || grantedAt.After(groupBoundary))
|
|
}
|
|
|
|
func iamMappingParentPath(path string) string {
|
|
kind, name, ok := strings.Cut(strings.TrimPrefix(path, iamConfigPolicyDBPrefix), "/")
|
|
if !ok {
|
|
return ""
|
|
}
|
|
name = strings.TrimSuffix(name, ".json")
|
|
switch kind {
|
|
case "users", "sts-users":
|
|
return getUserIdentityPath(name, regUser)
|
|
case "service-accounts":
|
|
return getUserIdentityPath(name, svcUser)
|
|
case "groups":
|
|
return getGroupInfoPath(name)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (idx *iamRevisionIndex) mappingAllowed(path string, mp MappedPolicy) bool {
|
|
if mp.Deleted || idx.get(path).Deleted {
|
|
return false
|
|
}
|
|
r := idx.get(iamMappingParentPath(path))
|
|
return !r.Deleted && (r.RevokedBefore.IsZero() || mp.UpdatedAt.After(r.RevokedBefore))
|
|
}
|
|
|
|
// Apply the persisted commit boundary even before dependent cache cleanup has
|
|
// completed. The map namespace is part of the authorization record's identity.
|
|
func (c *iamCache) cachedMappedPolicy(name string, userType IAMUserType, isGroup bool) (MappedPolicy, bool) {
|
|
var mp MappedPolicy
|
|
var ok bool
|
|
switch {
|
|
case isGroup:
|
|
mp, ok = c.iamGroupPolicyMap.Load(name)
|
|
case userType == stsUser:
|
|
mp, ok = c.iamSTSPolicyMap.Load(name)
|
|
default:
|
|
mp, ok = c.iamUserPolicyMap.Load(name)
|
|
}
|
|
if !ok || !c.revisions.mappingAllowed(getMappedPolicyPath(name, userType, isGroup), mp) {
|
|
return MappedPolicy{}, false
|
|
}
|
|
return mp, true
|
|
}
|
|
|
|
func (idx *iamRevisionIndex) parentRevision(user string) iamRevision {
|
|
if idx == nil {
|
|
return iamRevision{}
|
|
}
|
|
idx.mu.RLock()
|
|
p := idx.parents[user]
|
|
idx.mu.RUnlock()
|
|
return iamRevision{Deleted: p.deleted, RevokedBefore: p.before}
|
|
}
|