mirror of
https://github.com/pgsty/minio.git
synced 2026-11-10 17:45:32 +03:00
fix(iam): refresh LDAP groups on the stored credential, not a stale snapshot
The hourly LDAP group refresh, which every server runs, reads the STS and service-account credentials from its own cache, queries the directory, and wrote each credential whose groups changed back whole through UpdateUserIdentity. A secret rotation or an edit made meanwhile, on this server or one whose notification it missed, was undone, and a credential deleted meanwhile came back enabled with its old secret. RefreshUserGroups replaces that writer, whose only caller was the refresh. Under the identity's cross-node revision lock it loads the stored record, refuses one that is absent, deleted, expired or now has another parent (whose groups the lookup did not compute), and otherwise changes only the groups of the stored record. A record without its access key gets it from the path, as the loaders do. A refused refresh drops the stale entry from the cache, which loads what is stored on next use; a written or unchanged one caches the stored record. An STS record keeps the lease of its token's remaining lifetime. Tests cover rotation and edit, deletion, a sibling server whose cache did not hear of a rotation or a deletion, recreation under the same and under another parent, unchanged groups, and a stored record without its access key. Written back as the old snapshot write, every case fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
etcd "go.etcd.io/etcd/client/v3"
|
||||
"go.etcd.io/etcd/client/v3/concurrency"
|
||||
@@ -45,6 +46,22 @@ type iamGroupRevocation struct {
|
||||
// its revision so an older non-expiring credential cannot return.
|
||||
type iamExpireIdentity struct{}
|
||||
|
||||
// An LDAP group refresh sets the groups of the credential as it is stored
|
||||
// now. The refresh computes the groups from a snapshot of a server's cache
|
||||
// taken before a slow directory lookup; writing that snapshot back would
|
||||
// undo a rotation or an edit made meanwhile on any server, and bring back a
|
||||
// deleted credential. The refresh therefore changes only the groups, under
|
||||
// the identity's revision lock and on the stored record, and leaves alone a
|
||||
// credential that is gone, expired, or now has another parent, whose groups
|
||||
// the lookup did not compute.
|
||||
type iamGroupRefresh struct {
|
||||
UserIdentity // the record written, or found unchanged
|
||||
accessKey string
|
||||
parentUser string
|
||||
groups []string
|
||||
unchanged bool // the stored record already has the groups
|
||||
}
|
||||
|
||||
// The authoritative revocation is durable even if dependent cleanup fails.
|
||||
// Callers must publish it to sibling caches before returning the error.
|
||||
type iamCommittedCleanupError struct {
|
||||
@@ -252,6 +269,35 @@ func saveIAMRevision(ctx context.Context, store IAMStorageAPI, path string, item
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if op, ok := item.(*iamGroupRefresh); ok {
|
||||
var current UserIdentity
|
||||
err := store.loadIAMConfig(ctx, ¤t, path)
|
||||
if errors.Is(err, errConfigNotFound) {
|
||||
return errIAMStaleUpdate
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current.Credentials.AccessKey == "" {
|
||||
// As the identity loaders do: the path names the access key.
|
||||
current.Credentials.AccessKey = op.accessKey
|
||||
}
|
||||
if current.Deleted || current.Credentials.ParentUser != op.parentUser || current.Credentials.IsExpired() {
|
||||
return errIAMStaleUpdate
|
||||
}
|
||||
if set.CreateStringSet(current.Credentials.Groups...).Equals(set.CreateStringSet(op.groups...)) {
|
||||
op.UserIdentity, op.unchanged = current, true
|
||||
return nil
|
||||
}
|
||||
current.Credentials.Groups = op.groups
|
||||
current.UpdatedAt = time.Time{} // a new revision, as for any local edit
|
||||
op.UserIdentity = current
|
||||
item = &op.UserIdentity
|
||||
if strings.HasPrefix(path, iamConfigSTSPrefix) {
|
||||
// Keep the record expiring with its token, as SetTempUser stores it.
|
||||
opts = []options{{ttl: max(1, int64(time.Until(current.Credentials.Expiration).Seconds()))}}
|
||||
}
|
||||
}
|
||||
if _, expiring := item.(*iamExpireIdentity); expiring {
|
||||
sts := strings.HasPrefix(path, iamConfigSTSPrefix)
|
||||
if previous.Deleted {
|
||||
|
||||
Reference in New Issue
Block a user