Skip to content

Apply IAM patches when loading STS to avoid wrong access key formats #11931

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions localstack-core/localstack/services/iam/iam_patches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import threading
from typing import Optional

from moto.iam.models import (
AccessKey,
AWSManagedPolicy,
IAMBackend,
InlinePolicy,
Policy,
)
from moto.iam.models import Role as MotoRole
from moto.iam.policy_validation import VALID_STATEMENT_ELEMENTS

from localstack import config
from localstack.utils.patch import patch

ADDITIONAL_MANAGED_POLICIES = {
"AWSLambdaExecute": {
"Arn": "arn:aws:iam::aws:policy/AWSLambdaExecute",
"Path": "/",
"CreateDate": "2017-10-20T17:23:10+00:00",
"DefaultVersionId": "v4",
"Document": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["logs:*"],
"Resource": "arn:aws:logs:*:*:*",
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::*",
},
],
},
"UpdateDate": "2019-05-20T18:22:18+00:00",
}
}

IAM_PATCHED = False
IAM_PATCH_LOCK = threading.RLock()


def apply_iam_patches():
global IAM_PATCHED

# prevent patching multiple times, as this is called from both STS and IAM (for now)
with IAM_PATCH_LOCK:
if IAM_PATCHED:
return

IAM_PATCHED = True

# support service linked roles
moto_role_og_arn_prop = MotoRole.arn

@property
def moto_role_arn(self):
return getattr(self, "service_linked_role_arn", None) or moto_role_og_arn_prop.__get__(self)

MotoRole.arn = moto_role_arn

# Add missing managed polices
# TODO this might not be necessary
@patch(IAMBackend._init_aws_policies)
def _init_aws_policies_extended(_init_aws_policies, self):
loaded_policies = _init_aws_policies(self)
loaded_policies.extend(
[
AWSManagedPolicy.from_data(name, self.account_id, self.region_name, d)
for name, d in ADDITIONAL_MANAGED_POLICIES.items()
]
)
return loaded_policies

if "Principal" not in VALID_STATEMENT_ELEMENTS:
VALID_STATEMENT_ELEMENTS.append("Principal")

# patch policy __init__ to set document as attribute

@patch(Policy.__init__)
def policy__init__(
fn,
self,
name,
account_id,
region,
default_version_id=None,
description=None,
document=None,
**kwargs,
):
fn(self, name, account_id, region, default_version_id, description, document, **kwargs)
self.document = document

# patch unapply_policy

@patch(InlinePolicy.unapply_policy)
def inline_policy_unapply_policy(fn, self, backend):
try:
fn(self, backend)
except Exception:
# Actually role can be deleted before policy being deleted in cloudformation
pass

@patch(AccessKey.__init__)
def access_key__init__(
fn,
self,
user_name: Optional[str],
prefix: str,
account_id: str,
status: str = "Active",
**kwargs,
):
if not config.PARITY_AWS_ACCESS_KEY_ID:
prefix = "L" + prefix[1:]
fn(self, user_name, prefix, account_id, status, **kwargs)
139 changes: 3 additions & 136 deletions localstack-core/localstack/services/iam/provider.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
import json
import re
from datetime import datetime
from typing import Dict, List, Optional
from typing import Dict, List
from urllib.parse import quote

from moto.iam.models import (
AccessKey,
AWSManagedPolicy,
IAMBackend,
InlinePolicy,
Policy,
filter_items_with_path_prefix,
iam_backends,
)
from moto.iam.models import Role as MotoRole
from moto.iam.policy_validation import VALID_STATEMENT_ELEMENTS

from localstack import config
from localstack.aws.api import CommonServiceException, RequestContext, handler
from localstack.aws.api.iam import (
ActionNameListType,
Expand Down Expand Up @@ -66,37 +60,13 @@
)
from localstack.aws.connect import connect_to
from localstack.constants import INTERNAL_AWS_SECRET_ACCESS_KEY
from localstack.services.iam.iam_patches import apply_iam_patches
from localstack.services.moto import call_moto
from localstack.utils.aws.request_context import extract_access_key_id_from_auth_header
from localstack.utils.common import short_uid
from localstack.utils.patch import patch

SERVICE_LINKED_ROLE_PATH_PREFIX = "/aws-service-role"

ADDITIONAL_MANAGED_POLICIES = {
"AWSLambdaExecute": {
"Arn": "arn:aws:iam::aws:policy/AWSLambdaExecute",
"Path": "/",
"CreateDate": "2017-10-20T17:23:10+00:00",
"DefaultVersionId": "v4",
"Document": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["logs:*"],
"Resource": "arn:aws:logs:*:*:*",
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::*",
},
],
},
"UpdateDate": "2019-05-20T18:22:18+00:00",
}
}

POLICY_ARN_REGEX = re.compile(r"arn:[^:]+:iam::(?:\d{12}|aws):policy/.*")

Expand All @@ -107,7 +77,7 @@ def get_iam_backend(context: RequestContext) -> IAMBackend:

class IamProvider(IamApi):
def __init__(self):
apply_patches()
apply_iam_patches()

@handler("CreateRole", expand=False)
def create_role(
Expand Down Expand Up @@ -450,106 +420,3 @@ def attach_user_policy(
if not POLICY_ARN_REGEX.match(policy_arn):
raise InvalidInputException(f"ARN {policy_arn} is not valid.")
return call_moto(context=context)

# def get_user(
# self, context: RequestContext, user_name: existingUserNameType = None
# ) -> GetUserResponse:
# # TODO: The following migrates patch 'iam_response_get_user' as a provider function.
# # However, there are concerns with utilising 'aws_stack.extract_access_key_id_from_auth_header'
# # in place of 'moto.core.responses.get_current_user'.
# if not user_name:
# access_key_id = aws_stack.extract_access_key_id_from_auth_header(context.request.headers)
# moto_user = moto_iam_backend.get_user_from_access_key_id(access_key_id)
# if moto_user is None:
# moto_user = MotoUser("default_user")
# else:
# moto_user = moto_iam_backend.get_user(user_name)
#
# response_user_name = config.TEST_IAM_USER_NAME or moto_user.name
# response_user_id = config.TEST_IAM_USER_ID or moto_user.id
# moto_user = moto_iam_backend.users.get(response_user_name) or moto_user
# moto_tags = moto_iam_backend.tagger.list_tags_for_resource(moto_user.arn).get("Tags", [])
# response_tags = None
# if moto_tags:
# response_tags = [Tag(Key=t["Key"], Value=t["Value"]) for t in moto_tags]
#
# response_user = User()
# response_user["Path"] = moto_user.path
# response_user["UserName"] = response_user_name
# response_user["UserId"] = response_user_id
# response_user["Arn"] = moto_user.arn
# response_user["CreateDate"] = moto_user.create_date
# if moto_user.password_last_used:
# response_user["PasswordLastUsed"] = moto_user.password_last_used
# # response_user["PermissionsBoundary"] = # TODO
# if response_tags:
# response_user["Tags"] = response_tags
# return GetUserResponse(User=response_user)


def apply_patches():
# support service linked roles

@property
def moto_role_arn(self):
return getattr(self, "service_linked_role_arn", None) or moto_role_og_arn_prop.__get__(self)

moto_role_og_arn_prop = MotoRole.arn
MotoRole.arn = moto_role_arn

# Add missing managed polices
# TODO this might not be necessary
@patch(IAMBackend._init_aws_policies)
def _init_aws_policies_extended(_init_aws_policies, self):
loaded_policies = _init_aws_policies(self)
loaded_policies.extend(
[
AWSManagedPolicy.from_data(name, self.account_id, self.region_name, d)
for name, d in ADDITIONAL_MANAGED_POLICIES.items()
]
)
return loaded_policies

if "Principal" not in VALID_STATEMENT_ELEMENTS:
VALID_STATEMENT_ELEMENTS.append("Principal")

# patch policy __init__ to set document as attribute

@patch(Policy.__init__)
def policy__init__(
fn,
self,
name,
account_id,
region,
default_version_id=None,
description=None,
document=None,
**kwargs,
):
fn(self, name, account_id, region, default_version_id, description, document, **kwargs)
self.document = document

# patch unapply_policy

@patch(InlinePolicy.unapply_policy)
def inline_policy_unapply_policy(fn, self, backend):
try:
fn(self, backend)
except Exception:
# Actually role can be deleted before policy being deleted in cloudformation
pass

@patch(AccessKey.__init__)
def access_key__init__(
fn,
self,
user_name: Optional[str],
prefix: str,
account_id: str,
status: str = "Active",
**kwargs,
):
if not config.PARITY_AWS_ACCESS_KEY_ID:
prefix = "L" + prefix[1:]
fn(self, user_name, prefix, account_id, status, **kwargs)
4 changes: 4 additions & 0 deletions localstack-core/localstack/services/sts/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
tokenCodeType,
unrestrictedSessionPolicyDocumentType,
)
from localstack.services.iam.iam_patches import apply_iam_patches
from localstack.services.moto import call_moto
from localstack.services.plugins import ServiceLifecycleHook
from localstack.services.sts.models import sts_stores
Expand All @@ -27,6 +28,9 @@


class StsProvider(StsApi, ServiceLifecycleHook):
def __init__(self):
apply_iam_patches()

def get_caller_identity(self, context: RequestContext, **kwargs) -> GetCallerIdentityResponse:
response = call_moto(context)
if "user/moto" in response["Arn"] and "sts" in response["Arn"]:
Expand Down
2 changes: 1 addition & 1 deletion tests/aws/services/iam/test_iam.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from botocore.exceptions import ClientError

from localstack.aws.api.iam import Tag
from localstack.services.iam.provider import ADDITIONAL_MANAGED_POLICIES
from localstack.services.iam.iam_patches import ADDITIONAL_MANAGED_POLICIES
from localstack.testing.aws.util import create_client_with_keys, wait_for_user
from localstack.testing.pytest import markers
from localstack.utils.aws.arns import get_partition
Expand Down
Loading