From 6f10339dd2bff9acb92359642e5cbacac333ac17 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 13 Nov 2026 02:52:41 -0800 Subject: [PATCH] credential: migrate the legacy IAM file without overwriting newer entries The migration wrote every identity, service account and group from the snapshot the load read, over whatever the per-entry files held by then, so a revoke or delete landing in between was undone for good. It now migrates only the legacy file's own entries, creates each per-entry file only when none exists, and waits for the next load when the legacy file changed after the snapshot. Co-Authored-By: Claude Opus 5.5 --- .../filer_etc/filer_etc_identity.go | 76 +++++++++++++++---- 1 file changed, 63 insertions(+), 13 deletions(-) diff --git a/weed/credential/filer_etc/filer_etc_identity.go b/weed/credential/filer_etc/filer_etc_identity.go index cdf59ca714..24b594e801 100644 --- a/weed/credential/filer_etc/filer_etc_identity.go +++ b/weed/credential/filer_etc/filer_etc_identity.go @@ -1,10 +1,14 @@ package filer_etc import ( + "bytes" "context" + "crypto/md5" "encoding/json" + "errors" "fmt" "strings" + "time" "github.com/seaweedfs/seaweedfs/weed/credential" "github.com/seaweedfs/seaweedfs/weed/filer" @@ -54,7 +58,7 @@ func (store *FilerEtcStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3Ap // This ensures that all identities (including legacy ones) are written to individual files // and the legacy file is renamed. if foundLegacy { - if err := store.migrateToMultiFile(ctx, s3cfg); err != nil { + if err := store.migrateToMultiFile(ctx, content); err != nil { glog.Errorf("Failed to migrate IAM configuration to multi-file layout: %v", err) return s3cfg, nil } @@ -137,26 +141,43 @@ func (store *FilerEtcStore) loadFromMultiFile(ctx context.Context, s3cfg *iam_pb return hasIdentities, nil } -func (store *FilerEtcStore) migrateToMultiFile(ctx context.Context, s3cfg *iam_pb.S3ApiConfiguration) error { +func (store *FilerEtcStore) migrateToMultiFile(ctx context.Context, legacyContent []byte) error { + current, found, err := store.readInsideFiler(ctx, filer.IamConfigDirectory, IamLegacyIdentityFile) + if err != nil { + return err + } + if !found || !bytes.Equal(current, legacyContent) { + glog.V(0).Infof("%s changed during the IAM load, migrating it at the next load", IamLegacyIdentityFile) + return nil + } + legacy := &iam_pb.S3ApiConfiguration{} + if err := filer.ParseS3ConfigurationFromBytes(current, legacy); err != nil { + return err + } + glog.Infof("Migrating IAM configuration to multi-file layout...") - // 1. Save all identities - for _, identity := range s3cfg.Identities { - if err := store.saveIdentity(ctx, identity); err != nil { + // An existing per-entry file is newer than the legacy copy, and may have + // been rewritten or removed after this load read it: never overwrite it. + for _, identity := range legacy.Identities { + if err := store.createIfAbsent(ctx, IamIdentitiesDirectory, identity.Name, identity); err != nil { return err } } - - // 2. Save all service accounts - for _, sa := range s3cfg.ServiceAccounts { - if err := store.saveServiceAccount(ctx, sa); err != nil { + for _, sa := range legacy.ServiceAccounts { + if err := validateServiceAccountId(sa.Id); err != nil { + return err + } + if err := store.createIfAbsent(ctx, IamServiceAccountsDirectory, sa.Id, sa); err != nil { return err } } - - // 3. Save all groups - for _, g := range s3cfg.Groups { - if err := store.saveGroup(ctx, g); err != nil { + for _, g := range legacy.Groups { + g.Name = strings.TrimSpace(g.Name) + if g.Name == "" { + return fmt.Errorf("group name is required") + } + if err := store.createIfAbsent(ctx, IamGroupsDirectory, g.Name, g); err != nil { return err } } @@ -487,6 +508,35 @@ func (store *FilerEtcStore) saveIdentity(ctx context.Context, identity *iam_pb.I }) } +func (store *FilerEtcStore) createIfAbsent(ctx context.Context, subDir, name string, v any) error { + data, err := json.MarshalIndent(v, "", " ") + if err != nil { + return err + } + contentMd5 := md5.Sum(data) + return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { + err := filer_pb.CreateEntry(ctx, client, &filer_pb.CreateEntryRequest{ + Directory: filer.IamConfigDirectory + "/" + subDir, + Entry: &filer_pb.Entry{ + Name: name + ".json", + Content: data, + Attributes: &filer_pb.FuseAttributes{ + Mtime: time.Now().Unix(), + Crtime: time.Now().Unix(), + FileMode: uint32(0644), + FileSize: uint64(len(data)), + Md5: contentMd5[:], + }, + }, + OExcl: true, + }) + if errors.Is(err, filer_pb.ErrEntryAlreadyExists) { + return nil + } + return err + }) +} + func (store *FilerEtcStore) readInsideFiler(ctx context.Context, dir string, name string) ([]byte, bool, error) { var content []byte found := false