@ -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 ( )