Skip to content

Commit e5aa33d

Browse files
authored
Apply IAM patches when loading STS to avoid wrong access key formats (#11931)
1 parent 1abd2ff commit e5aa33d

File tree

4 files changed

+128
-137
lines changed

4 files changed

+128
-137
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import threading
2+
from typing import Optional
3+
4+
from moto.iam.models import (
5+
AccessKey,
6+
AWSManagedPolicy,
7+
IAMBackend,
8+
InlinePolicy,
9+
Policy,
10+
)
11+
from moto.iam.models import Role as MotoRole
12+
from moto.iam.policy_validation import VALID_STATEMENT_ELEMENTS
13+
14+
from localstack import config
15+
from localstack.utils.patch import patch
16+
17+
ADDITIONAL_MANAGED_POLICIES = {
18+
"AWSLambdaExecute": {
19+
"Arn": "arn:aws:iam::aws:policy/AWSLambdaExecute",
20+
"Path": "/",
21+
"CreateDate": "2017-10-20T17:23:10+00:00",
22+
"DefaultVersionId": "v4",
23+
"Document": {
24+
"Version": "2012-10-17",
25+
"Statement": [
26+
{
27+
"Effect": "Allow",
28+
"Action": ["logs:*"],
29+
"Resource": "arn:aws:logs:*:*:*",
30+
},
31+
{
32+
"Effect": "Allow",
33+
"Action": ["s3:GetObject", "s3:PutObject"],
34+
"Resource": "arn:aws:s3:::*",
35+
},
36+
],
37+
},
38+
"UpdateDate": "2019-05-20T18:22:18+00:00",
39+
}
40+
}
41+
42+
IAM_PATCHED = False
43+
IAM_PATCH_LOCK = threading.RLock()
44+
45+
46+
def apply_iam_patches():
47+
global IAM_PATCHED
48+
49+
# prevent patching multiple times, as this is called from both STS and IAM (for now)
50+
with IAM_PATCH_LOCK:
51+
if IAM_PATCHED:
52+
return
53+
54+
IAM_PATCHED = True
55+
56+
# support service linked roles
57+
moto_role_og_arn_prop = MotoRole.arn
58+
59+
@property
60+
def moto_role_arn(self):
61+
return getattr(self, "service_linked_role_arn", None) or moto_role_og_arn_prop.__get__(self)
62+
63+
MotoRole.arn = moto_role_arn
64+
65+
# Add missing managed polices
66+
# TODO this might not be necessary
67+
@patch(IAMBackend._init_aws_policies)
68+
def _init_aws_policies_extended(_init_aws_policies, self):
69+
loaded_policies = _init_aws_policies(self)
70+
loaded_policies.extend(
71+
[
72+
AWSManagedPolicy.from_data(name, self.account_id, self.region_name, d)
73+
for name, d in ADDITIONAL_MANAGED_POLICIES.items()
74+
]
75+
)
76+
return loaded_policies
77+
78+
if "Principal" not in VALID_STATEMENT_ELEMENTS:
79+
VALID_STATEMENT_ELEMENTS.append("Principal")
80+
81+
# patch policy __init__ to set document as attribute
82+
83+
@patch(Policy.__init__)
84+
def policy__init__(
85+
fn,
86+
self,
87+
name,
88+
account_id,
89+
region,
90+
default_version_id=None,
91+
description=None,
92+
document=None,
93+
**kwargs,
94+
):
95+
fn(self, name, account_id, region, default_version_id, description, document, **kwargs)
96+
self.document = document
97+
98+
# patch unapply_policy
99+
100+
@patch(InlinePolicy.unapply_policy)
101+
def inline_policy_unapply_policy(fn, self, backend):
102+
try:
103+
fn(self, backend)
104+
except Exception:
105+
# Actually role can be deleted before policy being deleted in cloudformation
106+
pass
107+
108+
@patch(AccessKey.__init__)
109+
def access_key__init__(
110+
fn,
111+
self,
112+
user_name: Optional[str],
113+
prefix: str,
114+
account_id: str,
115+
status: str = "Active",
116+
**kwargs,
117+
):
118+
if not config.PARITY_AWS_ACCESS_KEY_ID:
119+
prefix = "L" + prefix[1:]
120+
fn(self, user_name, prefix, account_id, status, **kwargs)

localstack-core/localstack/services/iam/provider.py

Lines changed: 3 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
11
import json
22
import re
33
from datetime import datetime
4-
from typing import Dict, List, Optional
4+
from typing import Dict, List
55
from urllib.parse import quote
66

77
from moto.iam.models import (
8-
AccessKey,
9-
AWSManagedPolicy,
108
IAMBackend,
11-
InlinePolicy,
12-
Policy,
139
filter_items_with_path_prefix,
1410
iam_backends,
1511
)
1612
from moto.iam.models import Role as MotoRole
17-
from moto.iam.policy_validation import VALID_STATEMENT_ELEMENTS
1813

19-
from localstack import config
2014
from localstack.aws.api import CommonServiceException, RequestContext, handler
2115
from localstack.aws.api.iam import (
2216
ActionNameListType,
@@ -66,37 +60,13 @@
6660
)
6761
from localstack.aws.connect import connect_to
6862
from localstack.constants import INTERNAL_AWS_SECRET_ACCESS_KEY
63+
from localstack.services.iam.iam_patches import apply_iam_patches
6964
from localstack.services.moto import call_moto
7065
from localstack.utils.aws.request_context import extract_access_key_id_from_auth_header
7166
from localstack.utils.common import short_uid
72-
from localstack.utils.patch import patch
7367

7468
SERVICE_LINKED_ROLE_PATH_PREFIX = "/aws-service-role"
7569

76-
ADDITIONAL_MANAGED_POLICIES = {
77-
"AWSLambdaExecute": {
78-
"Arn": "arn:aws:iam::aws:policy/AWSLambdaExecute",
79-
"Path": "/",
80-
"CreateDate": "2017-10-20T17:23:10+00:00",
81-
"DefaultVersionId": "v4",
82-
"Document": {
83-
"Version": "2012-10-17",
84-
"Statement": [
85-
{
86-
"Effect": "Allow",
87-
"Action": ["logs:*"],
88-
"Resource": "arn:aws:logs:*:*:*",
89-
},
90-
{
91-
"Effect": "Allow",
92-
"Action": ["s3:GetObject", "s3:PutObject"],
93-
"Resource": "arn:aws:s3:::*",
94-
},
95-
],
96-
},
97-
"UpdateDate": "2019-05-20T18:22:18+00:00",
98-
}
99-
}
10070

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

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

10878
class IamProvider(IamApi):
10979
def __init__(self):
110-
apply_patches()
80+
apply_iam_patches()
11181

11282
@handler("CreateRole", expand=False)
11383
def create_role(
@@ -450,106 +420,3 @@ def attach_user_policy(
450420
if not POLICY_ARN_REGEX.match(policy_arn):
451421
raise InvalidInputException(f"ARN {policy_arn} is not valid.")
452422
return call_moto(context=context)
453-
454-
# def get_user(
455-
# self, context: RequestContext, user_name: existingUserNameType = None
456-
# ) -> GetUserResponse:
457-
# # TODO: The following migrates patch 'iam_response_get_user' as a provider function.
458-
# # However, there are concerns with utilising 'aws_stack.extract_access_key_id_from_auth_header'
459-
# # in place of 'moto.core.responses.get_current_user'.
460-
# if not user_name:
461-
# access_key_id = aws_stack.extract_access_key_id_from_auth_header(context.request.headers)
462-
# moto_user = moto_iam_backend.get_user_from_access_key_id(access_key_id)
463-
# if moto_user is None:
464-
# moto_user = MotoUser("default_user")
465-
# else:
466-
# moto_user = moto_iam_backend.get_user(user_name)
467-
#
468-
# response_user_name = config.TEST_IAM_USER_NAME or moto_user.name
469-
# response_user_id = config.TEST_IAM_USER_ID or moto_user.id
470-
# moto_user = moto_iam_backend.users.get(response_user_name) or moto_user
471-
# moto_tags = moto_iam_backend.tagger.list_tags_for_resource(moto_user.arn).get("Tags", [])
472-
# response_tags = None
473-
# if moto_tags:
474-
# response_tags = [Tag(Key=t["Key"], Value=t["Value"]) for t in moto_tags]
475-
#
476-
# response_user = User()
477-
# response_user["Path"] = moto_user.path
478-
# response_user["UserName"] = response_user_name
479-
# response_user["UserId"] = response_user_id
480-
# response_user["Arn"] = moto_user.arn
481-
# response_user["CreateDate"] = moto_user.create_date
482-
# if moto_user.password_last_used:
483-
# response_user["PasswordLastUsed"] = moto_user.password_last_used
484-
# # response_user["PermissionsBoundary"] = # TODO
485-
# if response_tags:
486-
# response_user["Tags"] = response_tags
487-
# return GetUserResponse(User=response_user)
488-
489-
490-
def apply_patches():
491-
# support service linked roles
492-
493-
@property
494-
def moto_role_arn(self):
495-
return getattr(self, "service_linked_role_arn", None) or moto_role_og_arn_prop.__get__(self)
496-
497-
moto_role_og_arn_prop = MotoRole.arn
498-
MotoRole.arn = moto_role_arn
499-
500-
# Add missing managed polices
501-
# TODO this might not be necessary
502-
@patch(IAMBackend._init_aws_policies)
503-
def _init_aws_policies_extended(_init_aws_policies, self):
504-
loaded_policies = _init_aws_policies(self)
505-
loaded_policies.extend(
506-
[
507-
AWSManagedPolicy.from_data(name, self.account_id, self.region_name, d)
508-
for name, d in ADDITIONAL_MANAGED_POLICIES.items()
509-
]
510-
)
511-
return loaded_policies
512-
513-
if "Principal" not in VALID_STATEMENT_ELEMENTS:
514-
VALID_STATEMENT_ELEMENTS.append("Principal")
515-
516-
# patch policy __init__ to set document as attribute
517-
518-
@patch(Policy.__init__)
519-
def policy__init__(
520-
fn,
521-
self,
522-
name,
523-
account_id,
524-
region,
525-
default_version_id=None,
526-
description=None,
527-
document=None,
528-
**kwargs,
529-
):
530-
fn(self, name, account_id, region, default_version_id, description, document, **kwargs)
531-
self.document = document
532-
533-
# patch unapply_policy
534-
535-
@patch(InlinePolicy.unapply_policy)
536-
def inline_policy_unapply_policy(fn, self, backend):
537-
try:
538-
fn(self, backend)
539-
except Exception:
540-
# Actually role can be deleted before policy being deleted in cloudformation
541-
pass
542-
543-
@patch(AccessKey.__init__)
544-
def access_key__init__(
545-
fn,
546-
self,
547-
user_name: Optional[str],
548-
prefix: str,
549-
account_id: str,
550-
status: str = "Active",
551-
**kwargs,
552-
):
553-
if not config.PARITY_AWS_ACCESS_KEY_ID:
554-
prefix = "L" + prefix[1:]
555-
fn(self, user_name, prefix, account_id, status, **kwargs)

localstack-core/localstack/services/sts/provider.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
tokenCodeType,
1919
unrestrictedSessionPolicyDocumentType,
2020
)
21+
from localstack.services.iam.iam_patches import apply_iam_patches
2122
from localstack.services.moto import call_moto
2223
from localstack.services.plugins import ServiceLifecycleHook
2324
from localstack.services.sts.models import sts_stores
@@ -27,6 +28,9 @@
2728

2829

2930
class StsProvider(StsApi, ServiceLifecycleHook):
31+
def __init__(self):
32+
apply_iam_patches()
33+
3034
def get_caller_identity(self, context: RequestContext, **kwargs) -> GetCallerIdentityResponse:
3135
response = call_moto(context)
3236
if "user/moto" in response["Arn"] and "sts" in response["Arn"]:

tests/aws/services/iam/test_iam.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from botocore.exceptions import ClientError
66

77
from localstack.aws.api.iam import Tag
8-
from localstack.services.iam.provider import ADDITIONAL_MANAGED_POLICIES
8+
from localstack.services.iam.iam_patches import ADDITIONAL_MANAGED_POLICIES
99
from localstack.testing.aws.util import create_client_with_keys, wait_for_user
1010
from localstack.testing.pytest import markers
1111
from localstack.utils.aws.arns import get_partition

0 commit comments

Comments
 (0)