|
|
|
|
@@ -59,6 +59,13 @@ type IdentityAccessManagement struct {
|
|
|
|
|
|
|
|
|
|
// Bucket policy engine for evaluating bucket policies
|
|
|
|
|
policyEngine *BucketPolicyEngine
|
|
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
|
@@ -162,10 +169,17 @@ func NewIdentityAccessManagementWithStore(option *S3ApiServerOption, explicitSto
|
|
|
|
|
if err := iam.loadS3ApiConfigurationFromFile(option.Config); err != nil {
|
|
|
|
|
glog.Fatalf("fail to load config file %s: %v", option.Config, err)
|
|
|
|
|
}
|
|
|
|
|
// Check if any identities were actually loaded from the config file
|
|
|
|
|
iam.m.RLock()
|
|
|
|
|
|
|
|
|
|
// Track identity names from static config to protect them from dynamic updates
|
|
|
|
|
// Must be done under lock to avoid race conditions
|
|
|
|
|
iam.m.Lock()
|
|
|
|
|
iam.useStaticConfig = true
|
|
|
|
|
iam.staticIdentityNames = make(map[string]bool)
|
|
|
|
|
for _, identity := range iam.identities {
|
|
|
|
|
iam.staticIdentityNames[identity.Name] = true
|
|
|
|
|
}
|
|
|
|
|
configLoaded = len(iam.identities) > 0
|
|
|
|
|
iam.m.RUnlock()
|
|
|
|
|
iam.m.Unlock()
|
|
|
|
|
} else {
|
|
|
|
|
glog.V(3).Infof("no static config file specified... loading config from credential manager")
|
|
|
|
|
if err := iam.loadS3ApiConfigurationFromFiler(option); err != nil {
|
|
|
|
|
@@ -261,6 +275,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)
|
|
|
|
|
@@ -401,10 +431,245 @@ 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
|
|
|
|
|
existingIdx := -1
|
|
|
|
|
for i, existing := range identities {
|
|
|
|
|
if existing.Name == ident.Name {
|
|
|
|
|
existingIdx = i
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if existingIdx >= 0 {
|
|
|
|
|
// Before replacing, remove stale accessKeyIdent entries for the old identity
|
|
|
|
|
oldIdentity := identities[existingIdx]
|
|
|
|
|
for _, oldCred := range oldIdentity.Credentials {
|
|
|
|
|
// Only remove if it still points to this identity
|
|
|
|
|
if accessKeyIdent[oldCred.AccessKey] == oldIdentity {
|
|
|
|
|
delete(accessKeyIdent, oldCred.AccessKey)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Replace existing dynamic identity
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if this access key already exists in parent's credentials to avoid duplicates
|
|
|
|
|
alreadyExists := false
|
|
|
|
|
for _, existingCred := range parentIdent.Credentials {
|
|
|
|
|
if existingCred.AccessKey == sa.Credential.AccessKey {
|
|
|
|
|
alreadyExists = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if alreadyExists {
|
|
|
|
|
glog.V(3).Infof("Service account %s credential already exists for parent %s, skipping", sa.Id, sa.ParentUser)
|
|
|
|
|
// Ensure accessKeyIdent mapping is correct
|
|
|
|
|
accessKeyIdent[sa.Credential.AccessKey] = parentIdent
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (iam *IdentityAccessManagement) IsStaticConfig() bool {
|
|
|
|
|
iam.m.RLock()
|
|
|
|
|
defer iam.m.RUnlock()
|
|
|
|
|
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()
|
|
|
|
|
@@ -984,6 +1249,12 @@ func determineIAMAuthPath(sessionToken, principal, principalArn string) iamAuthP
|
|
|
|
|
// VerifyActionPermission checks if the identity is allowed to perform the action on the resource.
|
|
|
|
|
// It handles both traditional identities (via Actions) and IAM/STS identities (via Policy).
|
|
|
|
|
func (iam *IdentityAccessManagement) VerifyActionPermission(r *http.Request, identity *Identity, action Action, bucket, object string) s3err.ErrorCode {
|
|
|
|
|
// Fail closed if identity is nil
|
|
|
|
|
if identity == nil {
|
|
|
|
|
glog.V(3).Infof("VerifyActionPermission called with nil identity for action %s on %s/%s", action, bucket, object)
|
|
|
|
|
return s3err.ErrAccessDenied
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Traditional identities (with Actions from -s3.config) use legacy auth,
|
|
|
|
|
// JWT/STS identities (no Actions) use IAM authorization
|
|
|
|
|
if len(identity.Actions) > 0 {
|
|
|
|
|
|