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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user