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:
@@ -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).
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user