diff --git a/weed/s3api/auth_credentials.go b/weed/s3api/auth_credentials.go index 953e3311e..1e93bb632 100644 --- a/weed/s3api/auth_credentials.go +++ b/weed/s3api/auth_credentials.go @@ -62,6 +62,10 @@ type IdentityAccessManagement struct { // useStaticConfig indicates if the configuration was loaded from a static file useStaticConfig bool + + // staticIdentityNames tracks identity names loaded from the static config file + // These identities are immutable and cannot be updated by dynamic configuration + staticIdentityNames map[string]bool } type Identity struct { @@ -166,6 +170,15 @@ func NewIdentityAccessManagementWithStore(option *S3ApiServerOption, explicitSto glog.Fatalf("fail to load config file %s: %v", option.Config, err) } iam.useStaticConfig = true + + // Track identity names from static config to protect them from dynamic updates + iam.m.Lock() + iam.staticIdentityNames = make(map[string]bool) + for _, identity := range iam.identities { + iam.staticIdentityNames[identity.Name] = true + } + iam.m.Unlock() + // Check if any identities were actually loaded from the config file iam.m.RLock() configLoaded = len(iam.identities) > 0 @@ -265,6 +278,22 @@ func (iam *IdentityAccessManagement) LoadS3ApiConfigurationFromBytes(content []b } func (iam *IdentityAccessManagement) loadS3ApiConfiguration(config *iam_pb.S3ApiConfiguration) error { + // Check if we need to merge with existing static configuration + iam.m.RLock() + hasStaticConfig := iam.useStaticConfig && len(iam.staticIdentityNames) > 0 + iam.m.RUnlock() + + if hasStaticConfig { + // Merge mode: preserve static identities, add/update dynamic ones + return iam.mergeS3ApiConfiguration(config) + } + + // Normal mode: completely replace configuration + return iam.replaceS3ApiConfiguration(config) +} + +// replaceS3ApiConfiguration completely replaces the current configuration (used when no static config) +func (iam *IdentityAccessManagement) replaceS3ApiConfiguration(config *iam_pb.S3ApiConfiguration) error { var identities []*Identity var identityAnonymous *Identity accessKeyIdent := make(map[string]*Identity) @@ -405,6 +434,204 @@ func (iam *IdentityAccessManagement) loadS3ApiConfiguration(config *iam_pb.S3Api return nil } +// mergeS3ApiConfiguration merges dynamic configuration with existing static configuration +// Static identities (from file) are preserved and cannot be updated +// Dynamic identities (from filer/admin) can be added or updated +func (iam *IdentityAccessManagement) mergeS3ApiConfiguration(config *iam_pb.S3ApiConfiguration) error { + // Start with current configuration (which includes static identities) + iam.m.RLock() + identities := make([]*Identity, len(iam.identities)) + copy(identities, iam.identities) + identityAnonymous := iam.identityAnonymous + accessKeyIdent := make(map[string]*Identity) + for k, v := range iam.accessKeyIdent { + accessKeyIdent[k] = v + } + nameToIdentity := make(map[string]*Identity) + for k, v := range iam.nameToIdentity { + nameToIdentity[k] = v + } + accounts := make(map[string]*Account) + for k, v := range iam.accounts { + accounts[k] = v + } + emailAccount := make(map[string]*Account) + for k, v := range iam.emailAccount { + emailAccount[k] = v + } + staticNames := make(map[string]bool) + for k, v := range iam.staticIdentityNames { + staticNames[k] = v + } + iam.m.RUnlock() + + // Process accounts from dynamic config (can add new accounts) + for _, account := range config.Accounts { + if _, exists := accounts[account.Id]; !exists { + glog.V(3).Infof("adding dynamic account: name=%s, id=%s", account.DisplayName, account.Id) + accounts[account.Id] = &Account{ + Id: account.Id, + DisplayName: account.DisplayName, + EmailAddress: account.EmailAddress, + } + if account.EmailAddress != "" { + emailAccount[account.EmailAddress] = accounts[account.Id] + } + } + } + + // Ensure default accounts exist + if _, exists := accounts[AccountAdmin.Id]; !exists { + accounts[AccountAdmin.Id] = &Account{ + DisplayName: AccountAdmin.DisplayName, + EmailAddress: AccountAdmin.EmailAddress, + Id: AccountAdmin.Id, + } + emailAccount[AccountAdmin.EmailAddress] = accounts[AccountAdmin.Id] + } + if _, exists := accounts[AccountAnonymous.Id]; !exists { + accounts[AccountAnonymous.Id] = &Account{ + DisplayName: AccountAnonymous.DisplayName, + EmailAddress: AccountAnonymous.EmailAddress, + Id: AccountAnonymous.Id, + } + emailAccount[AccountAnonymous.EmailAddress] = accounts[AccountAnonymous.Id] + } + + // Process identities from dynamic config + for _, ident := range config.Identities { + // Skip static identities - they cannot be updated + if staticNames[ident.Name] { + glog.V(3).Infof("skipping static identity %s (immutable)", ident.Name) + continue + } + + glog.V(3).Infof("loading/updating dynamic identity %s (disabled=%v)", ident.Name, ident.Disabled) + t := &Identity{ + Name: ident.Name, + Credentials: nil, + Actions: nil, + PrincipalArn: generatePrincipalArn(ident.Name), + Disabled: ident.Disabled, + PolicyNames: ident.PolicyNames, + } + + switch { + case ident.Name == AccountAnonymous.Id: + t.Account = &AccountAnonymous + identityAnonymous = t + case ident.Account == nil: + t.Account = &AccountAdmin + default: + if account, ok := accounts[ident.Account.Id]; ok { + t.Account = account + } else { + t.Account = &AccountAdmin + glog.Warningf("identity %s is associated with a non exist account ID, the association is invalid", ident.Name) + } + } + + for _, action := range ident.Actions { + t.Actions = append(t.Actions, Action(action)) + } + for _, cred := range ident.Credentials { + t.Credentials = append(t.Credentials, &Credential{ + AccessKey: cred.AccessKey, + SecretKey: cred.SecretKey, + Status: cred.Status, + }) + accessKeyIdent[cred.AccessKey] = t + } + + // Update or add the identity + if existingIdx := -1; existingIdx >= 0 { + // Find and replace existing dynamic identity + for i, existing := range identities { + if existing.Name == ident.Name { + existingIdx = i + break + } + } + if existingIdx >= 0 { + identities[existingIdx] = t + } + } else { + // Add new dynamic identity + identities = append(identities, t) + } + nameToIdentity[t.Name] = t + } + + // Process service accounts from dynamic config + for _, sa := range config.ServiceAccounts { + if sa.Credential == nil { + continue + } + + // Skip disabled service accounts + if sa.Disabled { + glog.V(3).Infof("Skipping disabled service account %s", sa.Id) + continue + } + + // Find the parent identity + parentIdent, ok := nameToIdentity[sa.ParentUser] + if !ok { + glog.Warningf("Service account %s has non-existent parent user %s, skipping", sa.Id, sa.ParentUser) + continue + } + + // Skip if parent is a static identity (we don't modify static identities) + if staticNames[sa.ParentUser] { + glog.V(3).Infof("Skipping service account %s for static parent %s", sa.Id, sa.ParentUser) + continue + } + + // Add service account credential to parent identity + cred := &Credential{ + AccessKey: sa.Credential.AccessKey, + SecretKey: sa.Credential.SecretKey, + Status: sa.Credential.Status, + Expiration: sa.Expiration, + } + parentIdent.Credentials = append(parentIdent.Credentials, cred) + accessKeyIdent[sa.Credential.AccessKey] = parentIdent + glog.V(3).Infof("Loaded service account %s for dynamic parent %s (expiration: %d)", sa.Id, sa.ParentUser, sa.Expiration) + } + + iam.m.Lock() + // atomically switch + iam.identities = identities + iam.identityAnonymous = identityAnonymous + iam.accounts = accounts + iam.emailAccount = emailAccount + iam.accessKeyIdent = accessKeyIdent + iam.nameToIdentity = nameToIdentity + if !iam.isAuthEnabled { + iam.isAuthEnabled = len(identities) > 0 + } + iam.m.Unlock() + + // Log configuration summary + staticCount := len(staticNames) + dynamicCount := len(identities) - staticCount + glog.V(1).Infof("Merged config: %d static + %d dynamic identities = %d total, %d accounts, %d access keys. Auth enabled: %v", + staticCount, dynamicCount, len(identities), len(accounts), len(accessKeyIdent), iam.isAuthEnabled) + + if glog.V(2) { + glog.V(2).Infof("Access key to identity mapping:") + for accessKey, identity := range accessKeyIdent { + identityType := "dynamic" + if staticNames[identity.Name] { + identityType = "static" + } + glog.V(2).Infof(" %s -> %s (%s, actions: %d)", accessKey, identity.Name, identityType, len(identity.Actions)) + } + } + + return nil +} + func (iam *IdentityAccessManagement) isEnabled() bool { return iam.isAuthEnabled } @@ -413,6 +640,13 @@ func (iam *IdentityAccessManagement) IsStaticConfig() bool { return iam.useStaticConfig } +// IsStaticIdentity checks if an identity was loaded from the static config file +func (iam *IdentityAccessManagement) IsStaticIdentity(identityName string) bool { + iam.m.RLock() + defer iam.m.RUnlock() + return iam.staticIdentityNames[identityName] +} + func (iam *IdentityAccessManagement) lookupByAccessKey(accessKey string) (identity *Identity, cred *Credential, found bool) { iam.m.RLock() defer iam.m.RUnlock() diff --git a/weed/s3api/auth_credentials_subscribe.go b/weed/s3api/auth_credentials_subscribe.go index eaafa444e..e2d54e307 100644 --- a/weed/s3api/auth_credentials_subscribe.go +++ b/weed/s3api/auth_credentials_subscribe.go @@ -59,10 +59,6 @@ func (s3a *S3ApiServer) onIamConfigChange(dir string, oldEntry *filer_pb.Entry, if dir != filer.IamConfigDirectory { return nil } - if s3a.iam.IsStaticConfig() { - glog.V(0).Infof("Ignoring IAM config change because static configuration file is in use") - return nil - } // Handle deletion: reset to empty config if newEntry == nil && oldEntry != nil && oldEntry.Name == filer.IamIdentityFile {