s3api: add AttachUserPolicy/DetachUserPolicy/ListAttachedUserPolicies (#8379)

* iam: add XML responses for managed user policy APIs

* s3api: implement attach/detach/list attached user policies

* s3api: add embedded IAM tests for managed user policies

* iam: update CredentialStore interface and Manager for managed policies

Updated the `CredentialStore` interface to include `AttachUserPolicy`,
`DetachUserPolicy`, and `ListAttachedUserPolicies` methods.
The `CredentialManager` was updated to delegate these calls to the store.
Added common error variables for policy management.

* iam: implement managed policy methods in MemoryStore

Implemented `AttachUserPolicy`, `DetachUserPolicy`, and
`ListAttachedUserPolicies` in the MemoryStore.
Also ensured deep copying of identities includes PolicyNames.

* iam: implement managed policy methods in PostgresStore

Modified Postgres schema to include `policy_names` JSONB column in `users`.
Implemented `AttachUserPolicy`, `DetachUserPolicy`, and `ListAttachedUserPolicies`.
Updated user CRUD operations to handle policy names persistence.

* iam: implement managed policy methods in remaining stores

Implemented user policy management in:
- `FilerEtcStore` (partial implementation)
- `IamGrpcStore` (delegated via GetUser/UpdateUser)
- `PropagatingCredentialStore` (to broadcast updates)
Ensures cluster-wide consistency for policy attachments.

* s3api: refactor EmbeddedIamApi to use managed policy APIs

- Refactored `AttachUserPolicy`, `DetachUserPolicy`, and `ListAttachedUserPolicies`
  to use `e.credentialManager` directly.
- Fixed a critical error suppression bug in `ExecuteAction` that always
  returned success even on failure.
- Implemented robust error matching using string comparison fallbacks.
- Improved consistency by reloading configuration after policy changes.

* s3api: update and refine IAM integration tests

- Updated tests to use a real `MemoryStore`-backed `CredentialManager`.
- Refined test configuration synchronization using `sync.Once` and
  manual deep-copying to prevent state corruption.
- Improved `extractEmbeddedIamErrorCodeAndMessage` to handle more XML
  formats robustly.
- Adjusted test expectations to match current AWS IAM behavior.

* fix compilation

* visibility

* ensure 10 policies

* reload

* add integration tests

* Guard raft command registration

* Allow IAM actions in policy tests

* Validate gRPC policy attachments

* Revert Validate gRPC policy attachments

* Tighten gRPC policy attach/detach

* Improve IAM managed policy handling

* Improve managed policy filters
This commit is contained in:
Chris Lu
2026-02-19 12:26:27 -08:00
committed by GitHub
parent 6787dccace
commit 7b8df39cf7
13 changed files with 1153 additions and 232 deletions

View File

@@ -18,7 +18,7 @@ func (store *PostgresStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3Ap
config := &iam_pb.S3ApiConfiguration{}
// Query all users
rows, err := store.db.QueryContext(ctx, "SELECT username, email, account_data, actions FROM users")
rows, err := store.db.QueryContext(ctx, "SELECT username, email, account_data, actions, policy_names FROM users")
if err != nil {
return nil, fmt.Errorf("failed to query users: %w", err)
}
@@ -26,9 +26,9 @@ func (store *PostgresStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3Ap
for rows.Next() {
var username, email string
var accountDataJSON, actionsJSON []byte
var accountDataJSON, actionsJSON, policyNamesJSON []byte
if err := rows.Scan(&username, &email, &accountDataJSON, &actionsJSON); err != nil {
if err := rows.Scan(&username, &email, &accountDataJSON, &actionsJSON, &policyNamesJSON); err != nil {
return nil, fmt.Errorf("failed to scan user row: %w", err)
}
@@ -50,6 +50,13 @@ func (store *PostgresStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3Ap
}
}
// Parse policy names
if len(policyNamesJSON) > 0 {
if err := json.Unmarshal(policyNamesJSON, &identity.PolicyNames); err != nil {
return nil, fmt.Errorf("failed to unmarshal policy names for user %s: %v", username, err)
}
}
// Query credentials for this user
credRows, err := store.db.QueryContext(ctx, "SELECT access_key, secret_key FROM credentials WHERE username = $1", username)
if err != nil {
@@ -116,10 +123,19 @@ func (store *PostgresStore) SaveConfiguration(ctx context.Context, config *iam_p
}
}
// Marshal policy names
var policyNamesJSON []byte
if identity.PolicyNames != nil {
policyNamesJSON, err = json.Marshal(identity.PolicyNames)
if err != nil {
return fmt.Errorf("failed to marshal policy names for user %s: %v", identity.Name, err)
}
}
// Insert user
_, err := tx.ExecContext(ctx,
"INSERT INTO users (username, email, account_data, actions) VALUES ($1, $2, $3, $4)",
identity.Name, "", accountDataJSON, actionsJSON)
"INSERT INTO users (username, email, account_data, actions, policy_names) VALUES ($1, $2, $3, $4, $5)",
identity.Name, "", accountDataJSON, actionsJSON, policyNamesJSON)
if err != nil {
return fmt.Errorf("failed to insert user %s: %v", identity.Name, err)
}
@@ -178,10 +194,19 @@ func (store *PostgresStore) CreateUser(ctx context.Context, identity *iam_pb.Ide
}
}
// Marshal policy names
var policyNamesJSON []byte
if identity.PolicyNames != nil {
policyNamesJSON, err = json.Marshal(identity.PolicyNames)
if err != nil {
return fmt.Errorf("failed to marshal policy names: %w", err)
}
}
// Insert user
_, err = tx.ExecContext(ctx,
"INSERT INTO users (username, email, account_data, actions) VALUES ($1, $2, $3, $4)",
identity.Name, "", accountDataJSON, actionsJSON)
"INSERT INTO users (username, email, account_data, actions, policy_names) VALUES ($1, $2, $3, $4, $5)",
identity.Name, "", accountDataJSON, actionsJSON, policyNamesJSON)
if err != nil {
return fmt.Errorf("failed to insert user: %w", err)
}
@@ -205,11 +230,11 @@ func (store *PostgresStore) GetUser(ctx context.Context, username string) (*iam_
}
var email string
var accountDataJSON, actionsJSON []byte
var accountDataJSON, actionsJSON, policyNamesJSON []byte
err := store.db.QueryRowContext(ctx,
"SELECT email, account_data, actions FROM users WHERE username = $1",
username).Scan(&email, &accountDataJSON, &actionsJSON)
"SELECT email, account_data, actions, policy_names FROM users WHERE username = $1",
username).Scan(&email, &accountDataJSON, &actionsJSON, &policyNamesJSON)
if err != nil {
if err == sql.ErrNoRows {
return nil, credential.ErrUserNotFound
@@ -235,6 +260,13 @@ func (store *PostgresStore) GetUser(ctx context.Context, username string) (*iam_
}
}
// Parse policy names
if len(policyNamesJSON) > 0 {
if err := json.Unmarshal(policyNamesJSON, &identity.PolicyNames); err != nil {
return nil, fmt.Errorf("failed to unmarshal policy names: %w", err)
}
}
// Query credentials
rows, err := store.db.QueryContext(ctx, "SELECT access_key, secret_key FROM credentials WHERE username = $1", username)
if err != nil {
@@ -297,10 +329,19 @@ func (store *PostgresStore) UpdateUser(ctx context.Context, username string, ide
}
}
// Marshal policy names
var policyNamesJSON []byte
if identity.PolicyNames != nil {
policyNamesJSON, err = json.Marshal(identity.PolicyNames)
if err != nil {
return fmt.Errorf("failed to marshal policy names: %w", err)
}
}
// Update user
_, err = tx.ExecContext(ctx,
"UPDATE users SET email = $2, account_data = $3, actions = $4, updated_at = CURRENT_TIMESTAMP WHERE username = $1",
username, "", accountDataJSON, actionsJSON)
"UPDATE users SET email = $2, account_data = $3, actions = $4, policy_names = $5, updated_at = CURRENT_TIMESTAMP WHERE username = $1",
username, "", accountDataJSON, actionsJSON, policyNamesJSON)
if err != nil {
return fmt.Errorf("failed to update user: %w", err)
}
@@ -444,3 +485,81 @@ func (store *PostgresStore) DeleteAccessKey(ctx context.Context, username string
return nil
}
// AttachUserPolicy attaches a managed policy to a user by policy name
func (store *PostgresStore) AttachUserPolicy(ctx context.Context, username string, policyName string) error {
if !store.configured {
return fmt.Errorf("store not configured")
}
// Get user
identity, err := store.GetUser(ctx, username)
if err != nil {
return err
}
// Verify policy exists
policy, err := store.GetPolicy(ctx, policyName)
if err != nil {
return err
}
if policy == nil {
return credential.ErrPolicyNotFound
}
// Check if already attached
for _, p := range identity.PolicyNames {
if p == policyName {
return credential.ErrPolicyAlreadyAttached
}
}
// Append policy name and update
identity.PolicyNames = append(identity.PolicyNames, policyName)
return store.UpdateUser(ctx, username, identity)
}
// DetachUserPolicy detaches a managed policy from a user
func (store *PostgresStore) DetachUserPolicy(ctx context.Context, username string, policyName string) error {
if !store.configured {
return fmt.Errorf("store not configured")
}
// Get user
identity, err := store.GetUser(ctx, username)
if err != nil {
return err
}
// Find and remove policy
found := false
var newPolicyNames []string
for _, p := range identity.PolicyNames {
if p == policyName {
found = true
} else {
newPolicyNames = append(newPolicyNames, p)
}
}
if !found {
return credential.ErrPolicyNotAttached
}
identity.PolicyNames = newPolicyNames
return store.UpdateUser(ctx, username, identity)
}
// ListAttachedUserPolicies returns the list of policy names attached to a user
func (store *PostgresStore) ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error) {
if !store.configured {
return nil, fmt.Errorf("store not configured")
}
identity, err := store.GetUser(ctx, username)
if err != nil {
return nil, err
}
return identity.PolicyNames, nil
}

View File

@@ -93,12 +93,18 @@ func (store *PostgresStore) createTables() error {
email VARCHAR(255),
account_data JSONB,
actions JSONB,
policy_names JSONB DEFAULT '[]',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
`
// Migration: Add policy_names column if it doesn't exist (for existing installations)
addPolicyNamesColumn := `
ALTER TABLE users ADD COLUMN IF NOT EXISTS policy_names JSONB DEFAULT '[]';
`
// Create credentials table
credentialsTable := `
CREATE TABLE IF NOT EXISTS credentials (
@@ -139,6 +145,11 @@ func (store *PostgresStore) createTables() error {
return fmt.Errorf("failed to create users table: %w", err)
}
// Run migration to add policy_names column for existing installations
if _, err := store.db.Exec(addPolicyNamesColumn); err != nil {
return fmt.Errorf("failed to add policy_names column: %w", err)
}
if _, err := store.db.Exec(credentialsTable); err != nil {
return fmt.Errorf("failed to create credentials table: %w", err)
}