diff --git a/.gitignore b/.gitignore index fb9bd51d..02e812c5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ tests/file.tmp .eggs/ .mypy_cache/ *.tmp +.venv/ diff --git a/moto/core/utils.py b/moto/core/utils.py index dce9f675..921f64be 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -187,7 +187,7 @@ def iso_8601_datetime_with_milliseconds(datetime): def iso_8601_datetime_without_milliseconds(datetime): - return datetime.strftime("%Y-%m-%dT%H:%M:%S") + "Z" + return None if datetime is None else datetime.strftime("%Y-%m-%dT%H:%M:%S") + "Z" RFC1123 = "%a, %d %b %Y %H:%M:%S GMT" diff --git a/moto/iam/models.py b/moto/iam/models.py index 08a1eb36..d3907da2 100755 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -464,7 +464,7 @@ class AccessKey(BaseModel): self.secret_access_key = random_alphanumeric(40) self.status = "Active" self.create_date = datetime.utcnow() - self.last_used = datetime.utcnow() + self.last_used = None @property def created_iso_8601(self): @@ -676,20 +676,50 @@ class User(BaseModel): if len(self.access_keys) == 0: access_key_1_active = "false" access_key_1_last_rotated = "N/A" + access_key_1_last_used = "N/A" access_key_2_active = "false" access_key_2_last_rotated = "N/A" + access_key_2_last_used = "N/A" elif len(self.access_keys) == 1: - access_key_1_active = "true" - access_key_1_last_rotated = date_created.strftime(date_format) + access_key_1_active = ( + "true" if self.access_keys[0].status == "Active" else "false" + ) + access_key_1_last_rotated = self.access_keys[0].create_date.strftime( + date_format + ) + access_key_1_last_used = ( + "N/A" + if self.access_keys[0].last_used is None + else self.access_keys[0].last_used.strftime(date_format) + ) access_key_2_active = "false" access_key_2_last_rotated = "N/A" + access_key_2_last_used = "N/A" else: - access_key_1_active = "true" - access_key_1_last_rotated = date_created.strftime(date_format) - access_key_2_active = "true" - access_key_2_last_rotated = date_created.strftime(date_format) + access_key_1_active = ( + "true" if self.access_keys[0].status == "Active" else "false" + ) + access_key_1_last_rotated = self.access_keys[0].create_date.strftime( + date_format + ) + access_key_1_last_used = ( + "N/A" + if self.access_keys[0].last_used is None + else self.access_keys[0].last_used.strftime(date_format) + ) + access_key_2_active = ( + "true" if self.access_keys[1].status == "Active" else "false" + ) + access_key_2_last_rotated = self.access_keys[1].create_date.strftime( + date_format + ) + access_key_2_last_used = ( + "N/A" + if self.access_keys[1].last_used is None + else self.access_keys[1].last_used.strftime(date_format) + ) - return "{0},{1},{2},{3},{4},{5},not_supported,false,{6},{7},{8},{9},false,N/A,false,N/A".format( + return "{0},{1},{2},{3},{4},{5},not_supported,false,{6},{7},{8},not_supported,not_supported,{9},{10},{11},not_supported,not_supported,false,N/A,false,N/A\n".format( self.name, self.arn, date_created.strftime(date_format), @@ -698,8 +728,10 @@ class User(BaseModel): date_created.strftime(date_format), access_key_1_active, access_key_1_last_rotated, + access_key_1_last_used, access_key_2_active, access_key_2_last_rotated, + access_key_2_last_used, ) @@ -1799,7 +1831,7 @@ class IAMBackend(BaseBackend): def get_credential_report(self): if not self.credential_report: raise IAMReportNotPresentException("Credential report not present") - report = "user,arn,user_creation_time,password_enabled,password_last_used,password_last_changed,password_next_rotation,mfa_active,access_key_1_active,access_key_1_last_rotated,access_key_2_active,access_key_2_last_rotated,cert_1_active,cert_1_last_rotated,cert_2_active,cert_2_last_rotated\n" + report = "user,arn,user_creation_time,password_enabled,password_last_used,password_last_changed,password_next_rotation,mfa_active,access_key_1_active,access_key_1_last_rotated,access_key_1_last_used_date,access_key_1_last_used_region,access_key_1_last_used_service,access_key_2_active,access_key_2_last_rotated,access_key_2_last_used_date,access_key_2_last_used_region,access_key_2_last_used_service,cert_1_active,cert_1_last_rotated,cert_2_active,cert_2_last_rotated\n" for user in self.users: report += self.users[user].to_csv() return base64.b64encode(report.encode("ascii")).decode("ascii") diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 12501769..667a6d13 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -1779,7 +1779,11 @@ GET_ACCESS_KEY_LAST_USED_TEMPLATE = """ {{ user_name }} - {{ last_used }} + {% if last_used %} + {{ last_used }} + {% endif %} + N/A + N/A diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 99589543..6792d8f5 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -4,6 +4,7 @@ import json import boto import boto3 +import csv import os import sure # noqa import sys @@ -11,9 +12,10 @@ from boto.exception import BotoServerError from botocore.exceptions import ClientError from dateutil.tz import tzutc -from moto import mock_iam, mock_iam_deprecated -from moto.iam.models import aws_managed_policies +from moto import mock_iam, mock_iam_deprecated, settings from moto.core import ACCOUNT_ID +from moto.iam.models import aws_managed_policies +from moto.backends import get_backend from nose.tools import assert_raises, assert_equals from nose.tools import raises @@ -1215,6 +1217,69 @@ def test_boto3_get_credential_report(): report.should.match(r".*my-user.*") +@mock_iam +def test_boto3_get_credential_report_content(): + conn = boto3.client("iam", region_name="us-east-1") + username = "my-user" + conn.create_user(UserName=username) + key1 = conn.create_access_key(UserName=username)["AccessKey"] + conn.update_access_key( + UserName=username, AccessKeyId=key1["AccessKeyId"], Status="Inactive" + ) + key1 = conn.create_access_key(UserName=username)["AccessKey"] + timestamp = datetime.utcnow() + if not settings.TEST_SERVER_MODE: + iam_backend = get_backend("iam")["global"] + iam_backend.users[username].access_keys[1].last_used = timestamp + with assert_raises(ClientError): + conn.get_credential_report() + result = conn.generate_credential_report() + while result["State"] != "COMPLETE": + result = conn.generate_credential_report() + result = conn.get_credential_report() + report = result["Content"].decode("utf-8") + header = report.split("\n")[0] + header.should.equal( + "user,arn,user_creation_time,password_enabled,password_last_used,password_last_changed,password_next_rotation,mfa_active,access_key_1_active,access_key_1_last_rotated,access_key_1_last_used_date,access_key_1_last_used_region,access_key_1_last_used_service,access_key_2_active,access_key_2_last_rotated,access_key_2_last_used_date,access_key_2_last_used_region,access_key_2_last_used_service,cert_1_active,cert_1_last_rotated,cert_2_active,cert_2_last_rotated" + ) + report_dict = csv.DictReader(report.split("\n")) + user = next(report_dict) + user["user"].should.equal("my-user") + user["access_key_1_active"].should.equal("false") + user["access_key_1_last_rotated"].should.match(timestamp.strftime("%Y-%m-%d")) + user["access_key_1_last_used_date"].should.equal("N/A") + user["access_key_2_active"].should.equal("true") + if not settings.TEST_SERVER_MODE: + user["access_key_2_last_used_date"].should.match(timestamp.strftime("%Y-%m-%d")) + else: + user["access_key_2_last_used_date"].should.equal("N/A") + + +@mock_iam +def test_get_access_key_last_used_when_used(): + iam = boto3.resource("iam", region_name="us-east-1") + client = iam.meta.client + username = "test-user" + iam.create_user(UserName=username) + with assert_raises(ClientError): + client.get_access_key_last_used(AccessKeyId="non-existent-key-id") + create_key_response = client.create_access_key(UserName=username)["AccessKey"] + # Set last used date using the IAM backend. Moto currently does not have a mechanism for tracking usage of access keys + if not settings.TEST_SERVER_MODE: + timestamp = datetime.utcnow() + iam_backend = get_backend("iam")["global"] + iam_backend.users[username].access_keys[0].last_used = timestamp + resp = client.get_access_key_last_used( + AccessKeyId=create_key_response["AccessKeyId"] + ) + if not settings.TEST_SERVER_MODE: + datetime.strftime( + resp["AccessKeyLastUsed"]["LastUsedDate"], "%Y-%m-%d" + ).should.equal(timestamp.strftime("%Y-%m-%d")) + else: + resp["AccessKeyLastUsed"].should_not.contain("LastUsedDate") + + @requires_boto_gte("2.39") @mock_iam_deprecated() def test_managed_policy(): @@ -1382,7 +1447,7 @@ def test_update_access_key(): @mock_iam -def test_get_access_key_last_used(): +def test_get_access_key_last_used_when_unused(): iam = boto3.resource("iam", region_name="us-east-1") client = iam.meta.client username = "test-user" @@ -1393,10 +1458,7 @@ def test_get_access_key_last_used(): resp = client.get_access_key_last_used( AccessKeyId=create_key_response["AccessKeyId"] ) - - datetime.strftime( - resp["AccessKeyLastUsed"]["LastUsedDate"], "%Y-%m-%d" - ).should.equal(datetime.strftime(datetime.utcnow(), "%Y-%m-%d")) + resp["AccessKeyLastUsed"].should_not.contain("LastUsedDate") resp["UserName"].should.equal(create_key_response["UserName"])