Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.

Migrate IAM access keys#13838

Merged
dfangl merged 1 commit into
daniel/unc-273from
daniel/unc-231
Feb 25, 2026
Merged

Migrate IAM access keys#13838
dfangl merged 1 commit into
daniel/unc-273from
daniel/unc-231

Conversation

@dfangl

@dfangl dfangl commented Feb 24, 2026

Copy link
Copy Markdown
Member

Motivation

We need to migrate IAM user access keys from moto to LocalStack.

Changes

Operations Implemented

Operation Description
create_access_key Create a new access key for a user (max 2 per user)
delete_access_key Delete an access key
list_access_keys List access keys for a user (with pagination)
update_access_key Update access key status (Active/Inactive)
get_access_key_last_used Get last used information for an access key

New Entity: AccessKeyEntity

@dataclasses.dataclass
class AccessKeyEntity:
    """Wrapper for AccessKey with last used tracking."""
    access_key: AccessKey  # UserName, AccessKeyId, Status, SecretAccessKey, CreateDate
    last_used: AccessKeyLastUsed | None = None

Extended UserEntity

Added access_keys field as a dict for efficient lookup by access key ID:

access_keys: dict[str, AccessKeyEntity] = field(default_factory=dict)  # access_key_id -> entity

Extended IamStore

Added ACCESS_KEY_INDEX for efficient cross-user access key lookups:

ACCESS_KEY_INDEX: dict[str, str] = CrossRegionAttribute(default=dict)  # access_key_id -> user_name

Design Decisions

1. Access Key ID Generation

  • Uses generate_iam_identifier() from utils.py (same as role/user IDs)
  • Prefix determined by config.PARITY_AWS_ACCESS_KEY_ID:
    • AKIA for AWS parity mode
    • LKIA for LocalStack default
  • Total length: 20 characters

2. Secret Access Key Generation

  • 40-character random string
  • Character set: string.ascii_letters + string.digits + "+/"

3. User Name Derivation (Self-Referential Operations)

When user_name is not provided, the implementation:

  1. First tries to look up the access key ID from the request's Authorization header in ACCESS_KEY_INDEX
  2. If found, returns the associated user name directly
  3. If not found, raises ValidationError("Must specify userName when calling with non-User credentials")

This approach allows users to manage their own access keys without specifying their username.

4. Access Key Limit

  • Maximum 2 access keys per user (AWS behavior)
  • Enforced via LIMIT_ACCESS_KEYS_PER_USER = 2 constant
  • Raises LimitExceededException when exceeded

5. Delete User Validation

Updated delete_user to check for access keys before deletion:

  • Raises DeleteConflictException("Cannot delete entity, must delete access keys first.") if user has access keys

6. Error Handling

  • get_access_key_last_used with nonexistent key returns AccessDenied (not NoSuchEntity) to prevent enumeration attacks - this matches AWS behavior

Thread Safety

All access key operations use the existing self._user_lock for thread safety.

Known Limitations

  1. LastUsedDate Tracking: The last_used field is not populated because tracking when access keys are actually used requires integration with the authentication/STS layer. This is marked for future work.

Tests

Test Status Notes
test_access_key_lifecycle Passing Create, list, delete
test_access_key_update_status Passing Status changes
test_access_key_limit Passing 2-key limit enforcement
test_access_key_last_used Skipped Requires auth layer integration
test_access_key_errors Passing Error handling
test_access_key_deletion_without_username Passing Self-deletion

Related

@dfangl dfangl requested a review from pinzon as a code owner February 24, 2026 16:15
@dfangl dfangl added semver: minor Non-breaking changes which can be included in minor releases, but not in patch releases docs: skip Pull request does not require documentation changes notes: skip Pull request does not have to be mentioned in the release notes labels Feb 24, 2026
@github-actions

Copy link
Copy Markdown

LocalStack Community integration with Pro

  2 files  ±0    2 suites  ±0   4m 24s ⏱️ + 1m 34s
519 tests ±0  239 ✅  - 200  57 💤  - 7  223 ❌ +207 
521 runs  ±0  239 ✅  - 200  59 💤  - 7  223 ❌ +207 

For more details on these failures, see this check.

Results for commit 0ad2171. ± Comparison against base commit f8126b5.

@github-actions

Copy link
Copy Markdown

Test Results - Preflight, Unit

23 123 tests  ±0   21 252 ✅ ±0   6m 15s ⏱️ +6s
     1 suites ±0    1 871 💤 ±0 
     1 files   ±0        0 ❌ ±0 

Results for commit 0ad2171. ± Comparison against base commit f8126b5.

@github-actions

Copy link
Copy Markdown

Test Results (amd64) - Acceptance

7 tests  ±0   5 ✅ ±0   3m 2s ⏱️ ±0s
1 suites ±0   2 💤 ±0 
1 files   ±0   0 ❌ ±0 

Results for commit 0ad2171. ± Comparison against base commit f8126b5.

@github-actions

Copy link
Copy Markdown

Test Results (amd64) - Integration, Bootstrap

  5 files  ±0    5 suites  ±0   11m 37s ⏱️ -35s
543 tests ±0  470 ✅ +10  57 💤  - 7  16 ❌  - 3 
549 runs  ±0  470 ✅ +10  63 💤  - 7  16 ❌  - 3 

For more details on these failures, see this check.

Results for commit 0ad2171. ± Comparison against base commit f8126b5.

@pinzon pinzon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment on lines +2656 to +2665

def _generate_access_key_id(self, context: RequestContext) -> str:
"""Generate an access key ID with the appropriate prefix based on config."""
prefix = "AKIA" if config.PARITY_AWS_ACCESS_KEY_ID else "LKIA"
return generate_iam_identifier(context.account_id, prefix=prefix, total_length=20)

def _generate_secret_access_key(self) -> str:
"""Generate a 40-character random secret access key."""
charset = string.ascii_letters + string.digits + "+/"
return "".join(random.choices(charset, k=40))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: these 2 methods could be put in the new utils.py

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to, let's refactor those methods a together for all PRs - it's currently mostly in provider.py, we can bulk move them!

@dfangl dfangl merged commit dc8bb42 into daniel/unc-273 Feb 25, 2026
36 of 43 checks passed
@dfangl dfangl deleted the daniel/unc-231 branch February 25, 2026 09:18
dfangl added a commit that referenced this pull request Feb 25, 2026
dfangl added a commit that referenced this pull request Feb 26, 2026
dfangl added a commit that referenced this pull request Feb 26, 2026
dfangl added a commit that referenced this pull request Mar 4, 2026
dfangl added a commit that referenced this pull request Mar 6, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

docs: skip Pull request does not require documentation changes notes: skip Pull request does not have to be mentioned in the release notes semver: minor Non-breaking changes which can be included in minor releases, but not in patch releases

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants