Refactor Admin UI to use unified IAM storage and add MultipleFileStore (#8101)

* Refactor Admin UI to use unified IAM storage and add MultipleFileStore

* Address PR feedback: fix renames, error handling, and sync logic in FilerMultipleStore

* Address refined PR feedback: safe rename order, rollback logic, and structural sync refinement

* Optimize LoadConfiguration: use streaming callback for memory efficiency

* Refactor UpdateUser: log rollback failures during rename

* Implement PolicyManager for FilerMultipleStore

* include the filer_multiple backend configuration

* Implement cross-S3 synchronization and proper shutdown for all IAM backends

* Extract Admin UI refactoring to a separate PR
This commit is contained in:
Chris Lu
2026-01-23 20:12:59 -08:00
committed by GitHub
parent 535be3096b
commit f6318edbc9
9 changed files with 586 additions and 27 deletions

View File

@@ -61,6 +61,9 @@ type IdentityAccessManagement struct {
// Bucket policy engine for evaluating bucket policies
policyEngine *BucketPolicyEngine
// background polling
stopChan chan struct{}
// useStaticConfig indicates if the configuration was loaded from a static file
useStaticConfig bool
@@ -161,8 +164,8 @@ func NewIdentityAccessManagementWithStore(option *S3ApiServerOption, explicitSto
}
iam.credentialManager = credentialManager
iam.stopChan = make(chan struct{})
// First, try to load configurations from file or filer
// First, try to load configurations from file or filer
startConfigFile := option.Config
if startConfigFile == "" {
@@ -186,18 +189,22 @@ func NewIdentityAccessManagementWithStore(option *S3ApiServerOption, explicitSto
iam.m.Unlock()
}
// Always try to load/merge config from credential manager (filer)
// This ensures we get both static users (from file) and dynamic users (from filer)
// Always try to load/merge config from credential manager (filer/db)
// This ensures we get both static users (from file) and dynamic users (from backend)
glog.V(3).Infof("loading dynamic config from credential manager")
if err := iam.loadS3ApiConfigurationFromFiler(option); err != nil {
glog.Warningf("fail to load config: %v", err)
}
// Only consider config loaded if we actually have identities
// Don't block environment variable fallback just because filer call succeeded
// iam.m.RLock()
// configLoaded = len(iam.identities) > 0
// iam.m.RUnlock()
// Determine whether to start background polling for updates
// We poll if using a store that doesn't support real-time events (like Postgres)
if store := iam.credentialManager.GetStore(); store != nil {
storeName := store.GetName()
if storeName == credential.StoreTypePostgres {
glog.V(1).Infof("Starting background IAM polling for store: %s", storeName)
go iam.pollIamConfigChanges(1 * time.Minute)
}
}
// Check for AWS environment variables and merge them if present
// This serves as an in-memory "static" configuration
@@ -227,6 +234,31 @@ func NewIdentityAccessManagementWithStore(option *S3ApiServerOption, explicitSto
return iam
}
func (iam *IdentityAccessManagement) pollIamConfigChanges(interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := iam.LoadS3ApiConfigurationFromCredentialManager(); err != nil {
glog.Warningf("failed to reload IAM configuration via polling: %v", err)
}
case <-iam.stopChan:
return
}
}
}
func (iam *IdentityAccessManagement) Shutdown() {
if iam.stopChan != nil {
close(iam.stopChan)
}
if iam.credentialManager != nil {
iam.credentialManager.Shutdown()
}
}
// loadEnvironmentVariableCredentials loads AWS credentials from environment variables
// and adds them as a static admin identity. This function is idempotent and can be
// called multiple times (e.g., after configuration reloads).

View File

@@ -2,6 +2,7 @@ package s3api
import (
"errors"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/filer"
@@ -56,31 +57,48 @@ func (s3a *S3ApiServer) subscribeMetaEvents(clientName string, lastTsNs int64, p
// onIamConfigChange handles IAM config file changes (create, update, delete)
func (s3a *S3ApiServer) onIamConfigChange(dir string, oldEntry *filer_pb.Entry, newEntry *filer_pb.Entry) error {
if dir != filer.IamConfigDirectory {
return nil
}
if s3a.iam != nil && s3a.iam.IsStaticConfig() {
glog.V(1).Infof("Skipping IAM config update for static configuration")
return nil
}
// Handle deletion: reset to empty config
if newEntry == nil && oldEntry != nil && oldEntry.Name == filer.IamIdentityFile {
glog.V(1).Infof("IAM config file deleted, clearing identities")
if err := s3a.iam.LoadS3ApiConfigurationFromBytes([]byte{}); err != nil {
glog.Warningf("failed to clear IAM config on deletion: %v", err)
return err
// 1. Handle traditional single identity.json file
if dir == filer.IamConfigDirectory {
// Handle deletion: reset to empty config
if newEntry == nil && oldEntry != nil && oldEntry.Name == filer.IamIdentityFile {
glog.V(1).Infof("IAM config file deleted, clearing identities")
if err := s3a.iam.LoadS3ApiConfigurationFromBytes([]byte{}); err != nil {
glog.Warningf("failed to clear IAM config on deletion: %v", err)
return err
}
return nil
}
// Handle create/update
if newEntry != nil && newEntry.Name == filer.IamIdentityFile {
if err := s3a.iam.LoadS3ApiConfigurationFromBytes(newEntry.Content); err != nil {
return err
}
glog.V(1).Infof("updated %s/%s", dir, newEntry.Name)
}
return nil
}
// Handle create/update
if newEntry != nil && newEntry.Name == filer.IamIdentityFile {
if err := s3a.iam.LoadS3ApiConfigurationFromBytes(newEntry.Content); err != nil {
// 2. Handle multiple-file identities and policies
// Watch /etc/seaweedfs/identities and /etc/seaweedfs/policies
isIdentityDir := strings.HasPrefix(dir, "/etc/seaweedfs/identities")
isPolicyDir := strings.HasPrefix(dir, "/etc/seaweedfs/policies")
if isIdentityDir || isPolicyDir {
// For multiple-file mode, any change in these directories should trigger a full reload
// from the credential manager (which handles the details of loading from multiple files).
glog.V(1).Infof("IAM change detected in %s, reloading configuration", dir)
if err := s3a.iam.LoadS3ApiConfigurationFromCredentialManager(); err != nil {
glog.Errorf("failed to reload IAM configuration after change in %s: %v", dir, err)
return err
}
glog.V(1).Infof("updated %s/%s", dir, newEntry.Name)
}
return nil
}

View File

@@ -246,6 +246,12 @@ func NewS3ApiServerWithStore(router *mux.Router, option *S3ApiServerOption, expl
return s3ApiServer, nil
}
func (s3a *S3ApiServer) Shutdown() {
if s3a.iam != nil {
s3a.iam.Shutdown()
}
}
// getFilerAddress returns the current active filer address
// Uses FilerClient's tracked current filer which is updated on successful operations
// This provides better availability than always using the first filer
@@ -675,14 +681,14 @@ func (s3a *S3ApiServer) registerRouter(router *mux.Router) {
// ParseForm() consumes the request body, which breaks AWS Signature V4 verification
// for IAM requests. The signature must be calculated on the original body.
// Instead, check only the query string for the Action parameter.
// For IAM requests, the Action is typically in the POST body, not query string
// So we match all authenticated POST / requests and let AuthIam validate them
// This is safe because:
// 1. STS actions are excluded (handled by separate STS routes)
// 2. S3 operations don't POST to / (they use /<bucket> or /<bucket>/<key>)
// 3. IAM operations all POST to /
// Only exclude STS actions which might be in query string
action := r.URL.Query().Get("Action")
if action == "AssumeRole" || action == "AssumeRoleWithWebIdentity" || action == "AssumeRoleWithLDAPIdentity" {
@@ -695,7 +701,7 @@ func (s3a *S3ApiServer) registerRouter(router *mux.Router) {
apiRouter.Methods(http.MethodPost).Path("/").MatcherFunc(iamMatcher).
HandlerFunc(track(s3a.embeddedIam.AuthIam(s3a.cb.Limit(s3a.embeddedIam.DoActions, ACTION_WRITE)), "IAM"))
glog.V(1).Infof("Embedded IAM API enabled on S3 port")
}