Skip to content

Commit af18374

Browse files
authored
IAM: generate_credentials_report() now shows active certificates (#8032)
1 parent ed926f0 commit af18374

File tree

5 files changed

+263
-85
lines changed

5 files changed

+263
-85
lines changed

moto/iam/models.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1519,6 +1519,13 @@ def to_csv(self) -> str:
15191519
else self.access_keys[1].last_used.strftime(date_format)
15201520
)
15211521

1522+
cert1_active = cert2_active = False
1523+
if len(self.signing_certificates) > 0:
1524+
cert1 = list(self.signing_certificates.values())[0]
1525+
cert1_active = cert1.status == "Active"
1526+
if len(self.signing_certificates) > 1:
1527+
cert2 = list(self.signing_certificates.values())[1]
1528+
cert2_active = cert2.status == "Active"
15221529
fields = [
15231530
self.name,
15241531
self.arn,
@@ -1538,9 +1545,9 @@ def to_csv(self) -> str:
15381545
access_key_2_last_used,
15391546
"not_supported",
15401547
"not_supported",
1541-
"false",
1548+
"true" if cert1_active else "false",
15421549
"N/A",
1543-
"false",
1550+
"true" if cert2_active else "false",
15441551
"N/A",
15451552
]
15461553
return ",".join(fields) + "\n"
@@ -2719,6 +2726,13 @@ def upload_signing_certificate(
27192726
except Exception:
27202727
raise MalformedCertificate(body)
27212728

2729+
if (
2730+
len(user.signing_certificates)
2731+
>= self.account_summary._signing_certificates_per_user_quota
2732+
):
2733+
raise IAMLimitExceededException(
2734+
"Cannot exceed quota for CertificatesPerUser: 2"
2735+
)
27222736
user.signing_certificates[cert_id] = SigningCertificate(
27232737
cert_id, user_name, body
27242738
)

tests/test_iam/__init__.py

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import os
21
from functools import wraps
2+
from uuid import uuid4
3+
4+
import boto3
35

46
from moto import mock_aws
7+
from tests import allow_aws_request
58

69

7-
def iam_aws_verified(func):
10+
def iam_aws_verified(create_user: bool = False):
811
"""
912
Function that is verified to work against AWS.
1013
Can be run against AWS at any time by setting:
@@ -13,16 +16,34 @@ def iam_aws_verified(func):
1316
If this environment variable is not set, the function runs in a `mock_aws` context.
1417
"""
1518

16-
@wraps(func)
17-
def pagination_wrapper():
18-
allow_aws_request = (
19-
os.environ.get("MOTO_TEST_ALLOW_AWS_REQUEST", "false").lower() == "true"
20-
)
19+
def inner(func):
20+
def create_user_and_invoke_test():
21+
client = boto3.client("iam", "us-east-1")
22+
user_name = f"testuser_{str(uuid4())[0:6]}"
23+
try:
24+
if create_user:
25+
client.create_user(UserName=user_name)
26+
return func(user_name=user_name)
27+
finally:
28+
if create_user:
29+
certificates = client.list_signing_certificates(UserName=user_name)[
30+
"Certificates"
31+
]
32+
33+
for cert in certificates:
34+
client.delete_signing_certificate(
35+
UserName=user_name, CertificateId=cert["CertificateId"]
36+
)
37+
client.delete_user(UserName=user_name)
38+
39+
@wraps(func)
40+
def pagination_wrapper():
41+
if allow_aws_request():
42+
return create_user_and_invoke_test()
43+
else:
44+
with mock_aws():
45+
return create_user_and_invoke_test()
2146

22-
if allow_aws_request:
23-
return func()
24-
else:
25-
with mock_aws():
26-
return func()
47+
return pagination_wrapper
2748

28-
return pagination_wrapper
49+
return inner

tests/test_iam/test_iam.py

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2542,74 +2542,6 @@ def test_get_account_authorization_details():
25422542
assert len(result["Policies"]) > 1
25432543

25442544

2545-
@mock_aws
2546-
def test_signing_certs():
2547-
client = boto3.client("iam", region_name="us-east-1")
2548-
2549-
# Create the IAM user first:
2550-
client.create_user(UserName="testing")
2551-
2552-
# Upload the cert:
2553-
resp = client.upload_signing_certificate(
2554-
UserName="testing", CertificateBody=MOCK_CERT
2555-
)["Certificate"]
2556-
cert_id = resp["CertificateId"]
2557-
2558-
assert resp["UserName"] == "testing"
2559-
assert resp["Status"] == "Active"
2560-
assert resp["CertificateBody"] == MOCK_CERT
2561-
assert resp["CertificateId"]
2562-
2563-
# Upload a the cert with an invalid body:
2564-
with pytest.raises(ClientError) as ce:
2565-
client.upload_signing_certificate(
2566-
UserName="testing", CertificateBody="notacert"
2567-
)
2568-
assert ce.value.response["Error"]["Code"] == "MalformedCertificate"
2569-
2570-
# Upload with an invalid user:
2571-
with pytest.raises(ClientError):
2572-
client.upload_signing_certificate(
2573-
UserName="notauser", CertificateBody=MOCK_CERT
2574-
)
2575-
2576-
# Update:
2577-
client.update_signing_certificate(
2578-
UserName="testing", CertificateId=cert_id, Status="Inactive"
2579-
)
2580-
2581-
with pytest.raises(ClientError):
2582-
client.update_signing_certificate(
2583-
UserName="notauser", CertificateId=cert_id, Status="Inactive"
2584-
)
2585-
2586-
fake_id_name = "x" * 32
2587-
with pytest.raises(ClientError) as ce:
2588-
client.update_signing_certificate(
2589-
UserName="testing", CertificateId=fake_id_name, Status="Inactive"
2590-
)
2591-
2592-
assert (
2593-
ce.value.response["Error"]["Message"]
2594-
== f"The Certificate with id {fake_id_name} cannot be found."
2595-
)
2596-
2597-
# List the certs:
2598-
resp = client.list_signing_certificates(UserName="testing")["Certificates"]
2599-
assert len(resp) == 1
2600-
assert resp[0]["CertificateBody"] == MOCK_CERT
2601-
assert resp[0]["Status"] == "Inactive" # Changed with the update call above.
2602-
2603-
with pytest.raises(ClientError):
2604-
client.list_signing_certificates(UserName="notauser")
2605-
2606-
# Delete:
2607-
client.delete_signing_certificate(UserName="testing", CertificateId=cert_id)
2608-
2609-
with pytest.raises(ClientError):
2610-
client.delete_signing_certificate(UserName="notauser", CertificateId=cert_id)
2611-
2612-
26132545
@mock_aws()
26142546
def test_create_saml_provider():
26152547
conn = boto3.client("iam", region_name="us-east-1")

tests/test_iam/test_iam_cloudformation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1602,8 +1602,8 @@ def test_iam_roles():
16021602

16031603

16041604
@pytest.mark.aws_verified
1605-
@iam_aws_verified
1606-
def test_delete_instance_profile_with_existing_role():
1605+
@iam_aws_verified()
1606+
def test_delete_instance_profile_with_existing_role(user_name=None):
16071607
region = "us-east-1"
16081608
iam = boto3.client("iam", region_name=region)
16091609
iam_role_name = f"moto_{str(uuid4())[0:6]}"
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
from datetime import timedelta
2+
3+
import boto3
4+
import cryptography
5+
import pytest
6+
from botocore.exceptions import ClientError
7+
from cryptography.hazmat.primitives import hashes, serialization
8+
from cryptography.hazmat.primitives.asymmetric import rsa
9+
from cryptography.x509 import Name, NameAttribute
10+
from cryptography.x509.oid import NameOID
11+
12+
from moto.core.utils import utcnow
13+
from tests.test_iam import iam_aws_verified
14+
15+
16+
@iam_aws_verified(create_user=True)
17+
@pytest.mark.aws_verified
18+
def test_signing_certs(user_name=None):
19+
client = boto3.client("iam", region_name="us-east-1")
20+
certificate = create_certificate()
21+
22+
# Upload the cert:
23+
resp = client.upload_signing_certificate(
24+
UserName=user_name, CertificateBody=certificate
25+
)["Certificate"]
26+
cert_id = resp["CertificateId"]
27+
28+
assert resp["UserName"] == user_name
29+
assert resp["Status"] == "Active"
30+
assert resp["CertificateBody"] == certificate
31+
assert resp["CertificateId"]
32+
33+
# Update:
34+
client.update_signing_certificate(
35+
UserName=user_name, CertificateId=cert_id, Status="Inactive"
36+
)
37+
38+
# List the certs:
39+
resp = client.list_signing_certificates(UserName=user_name)["Certificates"]
40+
assert len(resp) == 1
41+
assert resp[0]["CertificateBody"] == certificate
42+
assert resp[0]["Status"] == "Inactive" # Changed with the update call above.
43+
44+
# Delete:
45+
client.delete_signing_certificate(UserName=user_name, CertificateId=cert_id)
46+
47+
48+
@iam_aws_verified(create_user=True)
49+
@pytest.mark.aws_verified
50+
def test_create_too_many_certificates(user_name=None):
51+
client = boto3.client("iam", region_name="us-east-1")
52+
certificate1 = create_certificate()
53+
certificate2 = create_certificate()
54+
certificate3 = create_certificate()
55+
56+
# Upload two certs
57+
cert_id1 = client.upload_signing_certificate(
58+
UserName=user_name, CertificateBody=certificate1
59+
)["Certificate"]["CertificateId"]
60+
cert_id2 = client.upload_signing_certificate(
61+
UserName=user_name, CertificateBody=certificate2
62+
)["Certificate"]["CertificateId"]
63+
assert cert_id1 != cert_id2
64+
65+
# Verify that a third certificate exceeds the limit
66+
with pytest.raises(ClientError) as exc:
67+
client.upload_signing_certificate(
68+
UserName=user_name, CertificateBody=certificate3
69+
)
70+
err = exc.value.response["Error"]
71+
assert err["Code"] == "LimitExceeded"
72+
assert err["Message"] == "Cannot exceed quota for CertificatesPerUser: 2"
73+
74+
75+
@iam_aws_verified(create_user=True)
76+
def test_retrieve_cert_details_using_credentials_report(user_name=None):
77+
"""
78+
AWS caches the Credentials Report for 4 hours
79+
Once you generate the report, you can request the report over and over again, but you'll always get that same report back in the next 4 hours
80+
# That makes it slightly impossible to verify this against AWS - that's why there is no `aws_verified`-marker
81+
"""
82+
client = boto3.client("iam", region_name="us-east-1")
83+
certificate1 = create_certificate()
84+
certificate2 = create_certificate()
85+
86+
# Upload the cert:
87+
cert_id1 = client.upload_signing_certificate(
88+
UserName=user_name, CertificateBody=certificate1
89+
)["Certificate"]["CertificateId"]
90+
91+
result = client.generate_credential_report()
92+
while result["State"] != "COMPLETE":
93+
result = client.generate_credential_report()
94+
report = client.get_credential_report()["Content"].decode("utf-8")
95+
96+
our_line = next(line for line in report.split("\n") if line.startswith(user_name))
97+
cert1_active = our_line.split(",")[-4]
98+
assert cert1_active == "true"
99+
cert2_active = our_line.split(",")[-2]
100+
assert cert2_active == "false"
101+
102+
client.upload_signing_certificate(UserName=user_name, CertificateBody=certificate2)
103+
104+
result = client.generate_credential_report()
105+
while result["State"] != "COMPLETE":
106+
result = client.generate_credential_report()
107+
report = client.get_credential_report()["Content"].decode("utf-8")
108+
our_line = next(line for line in report.split("\n") if line.startswith(user_name))
109+
110+
cert1_active = our_line.split(",")[-4]
111+
assert cert1_active == "true"
112+
cert2_active = our_line.split(",")[-2]
113+
assert cert2_active == "true"
114+
115+
# Set Certificate to Inactive
116+
client.update_signing_certificate(
117+
UserName=user_name, CertificateId=cert_id1, Status="Inactive"
118+
)
119+
120+
# Verify the credential report is updated
121+
result = client.generate_credential_report()
122+
while result["State"] != "COMPLETE":
123+
result = client.generate_credential_report()
124+
report = client.get_credential_report()["Content"].decode("utf-8")
125+
our_line = next(line for line in report.split("\n") if line.startswith(user_name))
126+
127+
cert1_active = our_line.split(",")[-4]
128+
assert cert1_active == "false"
129+
cert2_active = our_line.split(",")[-2]
130+
assert cert2_active == "true"
131+
132+
133+
@iam_aws_verified()
134+
@pytest.mark.aws_verified
135+
def test_upload_cert_for_unknown_user(user_name=None):
136+
client = boto3.client("iam", region_name="us-east-1")
137+
with pytest.raises(ClientError) as exc:
138+
client.upload_signing_certificate(
139+
UserName="notauser", CertificateBody=create_certificate()
140+
)
141+
err = exc.value.response["Error"]
142+
assert err["Code"] == "NoSuchEntity"
143+
assert err["Message"] == "The user with name notauser cannot be found."
144+
145+
with pytest.raises(ClientError) as exc:
146+
client.update_signing_certificate(
147+
UserName="notauser",
148+
CertificateId="asdfasdfasdfasdfasdfasdfasdasdf",
149+
Status="Inactive",
150+
)
151+
err = exc.value.response["Error"]
152+
assert err["Code"] == "NoSuchEntity"
153+
assert err["Message"] == "The user with name notauser cannot be found."
154+
155+
with pytest.raises(ClientError) as exc:
156+
client.list_signing_certificates(UserName="notauser")
157+
err = exc.value.response["Error"]
158+
assert err["Code"] == "NoSuchEntity"
159+
assert err["Message"] == "The user with name notauser cannot be found."
160+
161+
with pytest.raises(ClientError):
162+
client.delete_signing_certificate(UserName="notauser", CertificateId="x" * 32)
163+
err = exc.value.response["Error"]
164+
assert err["Code"] == "NoSuchEntity"
165+
assert err["Message"] == "The user with name notauser cannot be found."
166+
167+
168+
@iam_aws_verified(create_user=True)
169+
@pytest.mark.aws_verified
170+
def test_upload_invalid_certificate(user_name=None):
171+
client = boto3.client("iam", region_name="us-east-1")
172+
with pytest.raises(ClientError) as ce:
173+
client.upload_signing_certificate(
174+
UserName=user_name, CertificateBody="notacert"
175+
)
176+
assert ce.value.response["Error"]["Code"] == "MalformedCertificate"
177+
178+
179+
@iam_aws_verified(create_user=True)
180+
@pytest.mark.aws_verified
181+
def test_update_unknown_certificate(user_name=None):
182+
client = boto3.client("iam", region_name="us-east-1")
183+
fake_id_name = "x" * 32
184+
with pytest.raises(ClientError) as ce:
185+
client.update_signing_certificate(
186+
UserName=user_name, CertificateId=fake_id_name, Status="Inactive"
187+
)
188+
err = ce.value.response["Error"]
189+
190+
assert err["Message"] == f"The Certificate with id {fake_id_name} cannot be found."
191+
192+
193+
def create_certificate():
194+
key = rsa.generate_private_key(public_exponent=65537, key_size=2028)
195+
cert_subject = [NameAttribute(NameOID.COMMON_NAME, "iam.amazonaws.com")]
196+
issuer = [
197+
NameAttribute(NameOID.COUNTRY_NAME, "US"),
198+
NameAttribute(NameOID.ORGANIZATION_NAME, "Amazon"),
199+
NameAttribute(NameOID.COMMON_NAME, "Amazon RSA 2048 M01"),
200+
]
201+
cert = (
202+
cryptography.x509.CertificateBuilder()
203+
.subject_name(Name(cert_subject))
204+
.issuer_name(Name(issuer))
205+
.public_key(key.public_key())
206+
.serial_number(cryptography.x509.random_serial_number())
207+
.not_valid_before(utcnow())
208+
.not_valid_after(utcnow() + timedelta(days=365))
209+
.sign(key, hashes.SHA256())
210+
)
211+
return cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")

0 commit comments

Comments
 (0)