diff --git a/weed/s3api/auth_account_collapse_test.go b/weed/s3api/auth_account_collapse_test.go index cebca51fe..49380e33d 100644 --- a/weed/s3api/auth_account_collapse_test.go +++ b/weed/s3api/auth_account_collapse_test.go @@ -70,3 +70,62 @@ func TestCheckAccessByOwnershipDeniesNonOwner(t *testing.T) { owner.Header.Set(s3_constants.AmzAccountId, AccountAdmin.Id) assert.Equal(t, s3err.ErrNone, s3a.checkAccessByOwnership(owner, "b"), "the actual owner is still allowed") } + +// An account-less identity's synthesized account must be registered in the +// account lookup so its id resolves to a display name. Otherwise ACL grantee +// validation and owner display report the id as "not exists" — the regression +// where a canned PutObjectAcl granting to the caller's own account returned +// 400 InvalidRequest. +func TestUnscopedIdentityAccountResolvesByName(t *testing.T) { + resetMemoryStore() + + config := `{ + "identities": [ + {"name": "alice", "credentials": [{"accessKey": "alice_ak", "secretKey": "alice_sk"}], "actions": ["Read", "Write"]} + ] +}` + tmp, err := os.CreateTemp("", "s3-config-*.json") + require.NoError(t, err) + defer os.Remove(tmp.Name()) + _, err = tmp.WriteString(config) + require.NoError(t, err) + require.NoError(t, tmp.Close()) + + iam := NewIdentityAccessManagementWithStore(&S3ApiServerOption{Config: tmp.Name()}, nil, "memory") + + assert.Equal(t, "alice", iam.GetAccountNameById("alice"), + "account-less identity id must resolve to a display name for ACL/owner validation") +} + +// When an account is explicitly configured with the same id an account-less +// identity would synthesize, the identity must reuse that configured account so +// its custom display name/email are preserved. +func TestUnscopedIdentityReusesConfiguredAccount(t *testing.T) { + resetMemoryStore() + + config := `{ + "accounts": [ + {"id": "alice", "displayName": "Alice Smith", "emailAddress": "alice@example.com"} + ], + "identities": [ + {"name": "alice", "credentials": [{"accessKey": "alice_ak", "secretKey": "alice_sk"}], "actions": ["Read"]} + ] +}` + tmp, err := os.CreateTemp("", "s3-config-*.json") + require.NoError(t, err) + defer os.Remove(tmp.Name()) + _, err = tmp.WriteString(config) + require.NoError(t, err) + require.NoError(t, tmp.Close()) + + iam := NewIdentityAccessManagementWithStore(&S3ApiServerOption{Config: tmp.Name()}, nil, "memory") + + assert.Equal(t, "Alice Smith", iam.GetAccountNameById("alice"), + "explicitly configured account display name must be preserved") + + alice, _, found := iam.LookupByAccessKey("alice_ak") + require.True(t, found) + require.NotNil(t, alice.Account) + assert.Equal(t, "Alice Smith", alice.Account.DisplayName, + "identity must reuse the configured account, not the synthesized one") +} diff --git a/weed/s3api/auth_credentials.go b/weed/s3api/auth_credentials.go index f5fb56efb..1a11b490c 100644 --- a/weed/s3api/auth_credentials.go +++ b/weed/s3api/auth_credentials.go @@ -627,7 +627,18 @@ func (iam *IdentityAccessManagement) ReplaceS3ApiConfiguration(config *iam_pb.S3 t.Account = &AccountAnonymous identityAnonymous = t case ident.Account == nil: - t.Account = accountForUnscopedIdentity(t.Name) + // Account-less identities own resources under a distinct id derived + // from their name. Reuse an explicitly-configured account with that + // id if one exists (preserving its display name/email); otherwise + // synthesize one and register it so the id resolves via + // GetAccountNameById (ACL grantee validation, owner display). + synthesized := accountForUnscopedIdentity(t.Name) + if existing, ok := accounts[synthesized.Id]; ok { + t.Account = existing + } else { + t.Account = synthesized + accounts[synthesized.Id] = synthesized + } default: if account, ok := accounts[ident.Account.Id]; ok { t.Account = account @@ -846,7 +857,18 @@ func (iam *IdentityAccessManagement) MergeS3ApiConfiguration(config *iam_pb.S3Ap t.Account = &AccountAnonymous identityAnonymous = t case ident.Account == nil: - t.Account = accountForUnscopedIdentity(t.Name) + // Account-less identities own resources under a distinct id derived + // from their name. Reuse an explicitly-configured account with that + // id if one exists (preserving its display name/email); otherwise + // synthesize one and register it so the id resolves via + // GetAccountNameById (ACL grantee validation, owner display). + synthesized := accountForUnscopedIdentity(t.Name) + if existing, ok := accounts[synthesized.Id]; ok { + t.Account = existing + } else { + t.Account = synthesized + accounts[synthesized.Id] = synthesized + } default: if account, ok := accounts[ident.Account.Id]; ok { t.Account = account