package s3api import ( "context" "encoding/json" "errors" "fmt" "net/http" "os" "sync" "time" "github.com/seaweedfs/seaweedfs/weed/credential" "github.com/seaweedfs/seaweedfs/weed/filer" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/pb" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" // Import KMS providers to register them _ "github.com/seaweedfs/seaweedfs/weed/kms/aws" // _ "github.com/seaweedfs/seaweedfs/weed/kms/azure" // TODO: Fix Azure SDK compatibility issues _ "github.com/seaweedfs/seaweedfs/weed/kms/gcp" _ "github.com/seaweedfs/seaweedfs/weed/kms/local" _ "github.com/seaweedfs/seaweedfs/weed/kms/openbao" "google.golang.org/grpc" ) type Action string type Iam interface { Check(f http.HandlerFunc, actions ...Action) http.HandlerFunc } type IdentityAccessManagement struct { m sync.RWMutex identities []*Identity accessKeyIdent map[string]*Identity nameToIdentity map[string]*Identity // O(1) lookup by identity name accounts map[string]*Account emailAccount map[string]*Account hashes map[string]*sync.Pool hashCounters map[string]*int32 identityAnonymous *Identity hashMu sync.RWMutex domain string isAuthEnabled bool credentialManager *credential.CredentialManager filerClient filer_pb.SeaweedFilerClient grpcDialOption grpc.DialOption // IAM Integration for advanced features iamIntegration IAMIntegration // Bucket policy engine for evaluating bucket policies policyEngine *BucketPolicyEngine // background polling stopChan chan struct{} shutdownOnce sync.Once // 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 { Name string Account *Account Credentials []*Credential Actions []Action PolicyNames []string // Attached IAM policy names PrincipalArn string // ARN for IAM authorization (e.g., "arn:aws:iam::account-id:user/username") Disabled bool // User status: false = enabled (default), true = disabled Claims map[string]interface{} // JWT claims for policy substitution } // Account represents a system user, a system user can // configure multiple IAM-Users, IAM-Users can configure // permissions respectively, and each IAM-User can // configure multiple security credentials type Account struct { //Name is also used to display the "DisplayName" as the owner of the bucket or object DisplayName string EmailAddress string //Id is used to identify an Account when granting cross-account access(ACLs) to buckets and objects Id string } // Predefined Accounts var ( // AccountAdmin is used as the default account for IAM-Credentials access without Account configured AccountAdmin = Account{ DisplayName: "admin", EmailAddress: "admin@example.com", Id: s3_constants.AccountAdminId, } // AccountAnonymous is used to represent the account for anonymous access AccountAnonymous = Account{ DisplayName: "anonymous", EmailAddress: "anonymous@example.com", Id: s3_constants.AccountAnonymousId, } ) type Credential struct { AccessKey string SecretKey string Status string // Access key status: "Active" or "Inactive" (empty treated as "Active") Expiration int64 // Unix timestamp when credential expires (0 = no expiration) } // isCredentialExpired checks if a credential has expired func (c *Credential) isCredentialExpired() bool { return c.Expiration > 0 && c.Expiration < time.Now().Unix() } func NewIdentityAccessManagement(option *S3ApiServerOption) *IdentityAccessManagement { return NewIdentityAccessManagementWithStore(option, "") } func NewIdentityAccessManagementWithStore(option *S3ApiServerOption, explicitStore string) *IdentityAccessManagement { iam := &IdentityAccessManagement{ domain: option.DomainName, hashes: make(map[string]*sync.Pool), hashCounters: make(map[string]*int32), } // Always initialize credential manager with fallback to defaults credentialManager, err := credential.NewCredentialManagerWithDefaults(credential.CredentialStoreTypeName(explicitStore)) if err != nil { glog.Fatalf("failed to initialize credential manager: %v", err) } // For stores that need filer client details, set them temporarily // This will be updated to use FilerClient's GetCurrentFiler after FilerClient is created if store := credentialManager.GetStore(); store != nil { if filerFuncSetter, ok := store.(interface { SetFilerAddressFunc(func() pb.ServerAddress, grpc.DialOption) }); ok { // Temporary setup: use first filer until FilerClient is available // See s3api_server.go where this is updated to FilerClient.GetCurrentFiler if len(option.Filers) > 0 { getFiler := func() pb.ServerAddress { if len(option.Filers) > 0 { return option.Filers[0] } return "" } filerFuncSetter.SetFilerAddressFunc(getFiler, option.GrpcDialOption) glog.V(1).Infof("Credential store configured with temporary filer function (will be updated after FilerClient creation)") } } } iam.credentialManager = credentialManager iam.stopChan = make(chan struct{}) // First, try to load configurations from file or filer startConfigFile := option.Config if startConfigFile == "" { startConfigFile = option.IamConfig } if startConfigFile != "" { glog.V(3).Infof("loading static config file %s", startConfigFile) if err := iam.loadS3ApiConfigurationFromFile(startConfigFile); err != nil { glog.Fatalf("fail to load config file %s: %v", startConfigFile, err) } // 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 } iam.m.Unlock() } // 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) } // 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 iam.loadEnvironmentVariableCredentials() // Determine whether to enable S3 authentication based on configuration // For "weed mini" without any S3 config, default to allowing all access (isAuthEnabled = false) // If any credentials are configured (via file, filer, or env vars), enable authentication iam.m.Lock() iam.isAuthEnabled = len(iam.identities) > 0 iam.m.Unlock() if iam.isAuthEnabled { // Credentials were configured - enable authentication glog.V(1).Infof("S3 authentication enabled (%d identities configured)", len(iam.identities)) } else { // No credentials configured if startConfigFile != "" { // Config file was specified but contained no identities - this is unusual, log a warning glog.Warningf("S3 config file %s specified but no identities loaded - authentication disabled", startConfigFile) } else { // No config file and no identities - this is the normal allow-all case glog.V(1).Infof("S3 authentication disabled - no credentials configured (allowing all access)") } } 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() { iam.shutdownOnce.Do(func() { 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). func (iam *IdentityAccessManagement) loadEnvironmentVariableCredentials() { accessKeyId := os.Getenv("AWS_ACCESS_KEY_ID") secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY") if accessKeyId == "" || secretAccessKey == "" { return } // Create environment variable identity name identityNameSuffix := accessKeyId if len(accessKeyId) > 8 { identityNameSuffix = accessKeyId[:8] } identityName := "admin-" + identityNameSuffix // Create admin identity with environment variable credentials envIdentity := &Identity{ Name: identityName, Account: &AccountAdmin, Credentials: []*Credential{ { AccessKey: accessKeyId, SecretKey: secretAccessKey, }, }, Actions: []Action{ s3_constants.ACTION_ADMIN, }, } iam.m.Lock() defer iam.m.Unlock() // Initialize maps if they are nil if iam.staticIdentityNames == nil { iam.staticIdentityNames = make(map[string]bool) } if iam.accessKeyIdent == nil { iam.accessKeyIdent = make(map[string]*Identity) } if iam.nameToIdentity == nil { iam.nameToIdentity = make(map[string]*Identity) } // Check if identity already exists (avoid duplicates) exists := false for _, ident := range iam.identities { if ident.Name == identityName { exists = true break } } if !exists { glog.Infof("Added admin identity from AWS environment variables: name=%s, accessKey=%s", envIdentity.Name, accessKeyId) // Add to identities list iam.identities = append(iam.identities, envIdentity) // Update credential mappings iam.accessKeyIdent[accessKeyId] = envIdentity iam.nameToIdentity[envIdentity.Name] = envIdentity // Treat env var identity as static (immutable) iam.staticIdentityNames[envIdentity.Name] = true // Ensure defaults exist if iam.accounts == nil { iam.accounts = make(map[string]*Account) } iam.accounts[AccountAdmin.Id] = &AccountAdmin iam.accounts[AccountAnonymous.Id] = &AccountAnonymous if iam.emailAccount == nil { iam.emailAccount = make(map[string]*Account) } iam.emailAccount[AccountAdmin.EmailAddress] = &AccountAdmin iam.emailAccount[AccountAnonymous.EmailAddress] = &AccountAnonymous } } func (iam *IdentityAccessManagement) loadS3ApiConfigurationFromFiler(option *S3ApiServerOption) (err error) { // Try to load configuration with retries to handle transient connectivity issues during startup for i := 0; i < 10; i++ { err = iam.doLoadS3ApiConfigurationFromFiler(option) if err == nil { return nil } if errors.Is(err, filer_pb.ErrNotFound) { return err } glog.Warningf("fail to load config from filer (attempt %d/10): %v", i+1, err) time.Sleep(2 * time.Second) } return err } func (iam *IdentityAccessManagement) doLoadS3ApiConfigurationFromFiler(option *S3ApiServerOption) error { return iam.LoadS3ApiConfigurationFromCredentialManager() } func (iam *IdentityAccessManagement) loadS3ApiConfigurationFromFile(fileName string) error { content, readErr := os.ReadFile(fileName) if readErr != nil { glog.Warningf("fail to read %s : %v", fileName, readErr) return fmt.Errorf("fail to read %s : %v", fileName, readErr) } // Initialize KMS if configuration contains KMS settings if err := iam.initializeKMSFromConfig(content); err != nil { glog.Warningf("KMS initialization failed: %v", err) } return iam.LoadS3ApiConfigurationFromBytes(content) } func (iam *IdentityAccessManagement) LoadS3ApiConfigurationFromBytes(content []byte) error { s3ApiConfiguration := &iam_pb.S3ApiConfiguration{} if err := filer.ParseS3ConfigurationFromBytes(content, s3ApiConfiguration); err != nil { glog.Warningf("unmarshal error: %v", err) return fmt.Errorf("unmarshal error: %w", err) } if err := filer.CheckDuplicateAccessKey(s3ApiConfiguration); err != nil { return err } if err := iam.loadS3ApiConfiguration(s3ApiConfiguration); err != nil { return err } return nil } 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) nameToIdentity := make(map[string]*Identity) accounts := make(map[string]*Account) emailAccount := make(map[string]*Account) foundAccountAdmin := false foundAccountAnonymous := false for _, account := range config.Accounts { glog.V(3).Infof("loading account name=%s, id=%s", account.DisplayName, account.Id) accounts[account.Id] = &Account{ Id: account.Id, DisplayName: account.DisplayName, EmailAddress: account.EmailAddress, } switch account.Id { case AccountAdmin.Id: foundAccountAdmin = true case AccountAnonymous.Id: foundAccountAnonymous = true } if account.EmailAddress != "" { emailAccount[account.EmailAddress] = accounts[account.Id] } } if !foundAccountAdmin { accounts[AccountAdmin.Id] = &Account{ DisplayName: AccountAdmin.DisplayName, EmailAddress: AccountAdmin.EmailAddress, Id: AccountAdmin.Id, } emailAccount[AccountAdmin.EmailAddress] = accounts[AccountAdmin.Id] } if !foundAccountAnonymous { accounts[AccountAnonymous.Id] = &Account{ DisplayName: AccountAnonymous.DisplayName, EmailAddress: AccountAnonymous.EmailAddress, Id: AccountAnonymous.Id, } emailAccount[AccountAnonymous.EmailAddress] = accounts[AccountAnonymous.Id] } for _, ident := range config.Identities { glog.V(3).Infof("loading identity %s (disabled=%v)", ident.Name, ident.Disabled) t := &Identity{ Name: ident.Name, Credentials: nil, Actions: nil, PrincipalArn: generatePrincipalArn(ident.Name), Disabled: ident.Disabled, // false (default) = enabled, true = 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, // Load access key status }) accessKeyIdent[cred.AccessKey] = t } identities = append(identities, t) nameToIdentity[t.Name] = t } // Load service accounts and add their credentials to the parent identity for _, sa := range config.ServiceAccounts { if sa.Credential == nil { continue } // Skip disabled service accounts - they should not be able to authenticate 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 } // Add service account credential to parent identity with expiration cred := &Credential{ AccessKey: sa.Credential.AccessKey, SecretKey: sa.Credential.SecretKey, Status: sa.Credential.Status, Expiration: sa.Expiration, // Populate expiration from service account } parentIdent.Credentials = append(parentIdent.Credentials, cred) accessKeyIdent[sa.Credential.AccessKey] = parentIdent glog.V(3).Infof("Loaded service account %s for 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 // Update authentication state based on whether identities exist // Once enabled, keep it enabled (one-way toggle) authJustEnabled := iam.updateAuthenticationState(len(identities)) iam.m.Unlock() if authJustEnabled { glog.V(1).Infof("S3 authentication enabled - credentials were added dynamically") } // Re-add environment variable credentials if they exist // This ensures env var credentials persist across configuration reloads iam.loadEnvironmentVariableCredentials() // Log configuration summary - always log to help debugging glog.Infof("Loaded %d identities, %d accounts, %d access keys. Auth enabled: %v", len(iam.identities), len(iam.accounts), len(iam.accessKeyIdent), iam.isAuthEnabled) if glog.V(2) { glog.V(2).Infof("Access key to identity mapping:") iam.m.RLock() for accessKey, identity := range iam.accessKeyIdent { glog.V(2).Infof(" %s -> %s (actions: %d)", accessKey, identity.Name, len(identity.Actions)) } iam.m.RUnlock() } 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 // Update authentication state based on whether identities exist // Once enabled, keep it enabled (one-way toggle) authJustEnabled := iam.updateAuthenticationState(len(identities)) iam.m.Unlock() if authJustEnabled { glog.V(1).Infof("S3 authentication enabled because credentials were added dynamically") } // 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 } // isEnabled reports whether S3 auth should be enforced for this server. // // Auth is considered enabled if either: // - we have any locally managed identities/credentials (iam.isAuthEnabled), or // - an external IAM integration has been configured (iam.iamIntegration != nil). func (iam *IdentityAccessManagement) isEnabled() bool { iam.m.RLock() defer iam.m.RUnlock() return iam.isAuthEnabled || iam.iamIntegration != nil } func (iam *IdentityAccessManagement) LookupByAccessKey(accessKey string) (identity *Identity, cred *Credential, found bool) { iam.m.RLock() defer iam.m.RUnlock() // 1. Try local lookup first if identity, found = iam.accessKeyIdent[accessKey]; found { // Check if identity is disabled if identity.Disabled { return nil, nil, false } // Find specific credential to check expiration and status for _, c := range identity.Credentials { if c.AccessKey == accessKey { // Check expiration if c.isCredentialExpired() { glog.V(1).Infof("Credential for %s has expired", identity.Name) return nil, nil, false } // Check status (defaults to Active if empty) if c.Status == "Inactive" { glog.V(1).Infof("Credential for %s is inactive", identity.Name) return nil, nil, false } return identity, c, true } } // Should not happen if accessKeyIdent is consistent, but safe fallback return identity, nil, true } // 2. If not found locally and IAM integration is enabled, try external lookup if iam.iamIntegration != nil { // Note: We release the read lock before calling external integration to avoid blocking // However, we're currently holding it. For now, since this interfaces might need locking internally, // we should probably let them handle it or assume they are thread-safe. // A better approach would be to unlock, call integration, then lock again if needed, // but simple lookup should be fine. // // TODO: Refactor IAM integration to be more decoupled } return nil, nil, false } func (iam *IdentityAccessManagement) LookupAnonymous() (identity *Identity, found bool) { iam.m.RLock() defer iam.m.RUnlock() if iam.identityAnonymous != nil { return iam.identityAnonymous, true } return nil, false } // GetUser returns the identity for the given username func (iam *IdentityAccessManagement) GetUser(username string) (*Identity, error) { iam.m.RLock() defer iam.m.RUnlock() identity, ok := iam.nameToIdentity[username] if !ok { return nil, fmt.Errorf("user not found: %s", username) } return identity, nil } // updateAuthenticationState updates the isAuthEnabled flag based on configuration // returns true if authentication was just enabled func (iam *IdentityAccessManagement) updateAuthenticationState(identityCount int) bool { wasEnabled := iam.isAuthEnabled // Enable if we have identities OR if we have external integration // But here we only check identities count isEnabled := identityCount > 0 iam.isAuthEnabled = isEnabled return !wasEnabled && isEnabled } // generatePrincipalArn generates a mock ARN for an identity func generatePrincipalArn(name string) string { // Format: arn:partition:service:region:account-id:resource-type/resource-id // We use a simplified format for embedded IAM return fmt.Sprintf("arn:aws:iam::%s:user/%s", s3_constants.AccountAdminId, name) } // initializeKMSFromConfig initializes KMS providers based on configuration func (iam *IdentityAccessManagement) initializeKMSFromConfig(content []byte) error { // Parse a minimal struct to get KMS config type KmsConfig struct { Kms struct { Type string `json:"type"` Properties map[string]string `json:"properties"` } `json:"kms"` } var config KmsConfig // Check if content is JSON or YAML/TOML? // The content passed to ParseS3ConfigurationFromBytes supports both via viper, // but here we might need to parse it manually if it's JSON. // Assuming JSON for s3 config.json if err := json.Unmarshal(content, &config); err != nil { // Failed to unmarshal as JSON, might be OK if it's not JSON return nil } if config.Kms.Type == "" { return nil } glog.V(1).Infof("Initializing KMS provider of type: %s", config.Kms.Type) // In a real implementation, we would pass these properties to the KMS factory // For now, we assume the side-effect imports have registered the providers // and we might need to configure them. // This part depends on how weed/kms is implemented. return nil } // Accessor methods for policy engine integration func (iam *IdentityAccessManagement) LoadS3ApiConfigurationFromCredentialManager() error { glog.V(1).Infof("IAM: reloading configuration from credential manager") glog.V(1).Infof("Loading S3 API configuration from credential manager") if iam.credentialManager != nil { config, err := iam.credentialManager.LoadConfiguration(context.Background()) if err != nil { return err } return iam.loadS3ApiConfiguration(config) } return nil }