diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 1773e61a3..735af6002 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -2257,7 +2257,7 @@
- [X] detach_user_policy
- [X] enable_mfa_device
- [ ] generate_credential_report
-- [ ] get_access_key_last_used
+- [X] get_access_key_last_used
- [X] get_account_authorization_details
- [ ] get_account_password_policy
- [ ] get_account_summary
diff --git a/Makefile b/Makefile
index f224d7091..de08c6f74 100644
--- a/Makefile
+++ b/Makefile
@@ -19,6 +19,7 @@ test: lint
rm -f .coverage
rm -rf cover
@nosetests -sv --with-coverage --cover-html ./tests/ $(TEST_EXCLUDE)
+
test_server:
@TEST_SERVER_MODE=true nosetests -sv --with-coverage --cover-html ./tests/
diff --git a/moto/iam/models.py b/moto/iam/models.py
index 0eeb55bea..5e4bb8263 100644
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -250,6 +250,10 @@ class AccessKey(BaseModel):
datetime.utcnow(),
"%Y-%m-%dT%H:%M:%SZ"
)
+ self.last_used = datetime.strftime(
+ datetime.utcnow(),
+ "%Y-%m-%dT%H:%M:%SZ"
+ )
def get_cfn_attribute(self, attribute_name):
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
@@ -379,21 +383,20 @@ class User(BaseModel):
return self.access_keys
def delete_access_key(self, access_key_id):
- for key in self.access_keys:
- if key.access_key_id == access_key_id:
- self.access_keys.remove(key)
- break
- else:
- raise IAMNotFoundException(
- "Key {0} not found".format(access_key_id))
+ key = self.get_access_key_by_id(access_key_id)
+ self.access_keys.remove(key)
def update_access_key(self, access_key_id, status):
+ key = self.get_access_key_by_id(access_key_id)
+ key.status = status
+
+ def get_access_key_by_id(self, access_key_id):
for key in self.access_keys:
if key.access_key_id == access_key_id:
- key.status = status
- break
+ return key
else:
- raise IAMNotFoundException("The Access Key with id {0} cannot be found".format(access_key_id))
+ raise IAMNotFoundException(
+ "The Access Key with id {0} cannot be found".format(access_key_id))
def get_cfn_attribute(self, attribute_name):
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
@@ -908,6 +911,24 @@ class IAMBackend(BaseBackend):
user = self.get_user(user_name)
user.update_access_key(access_key_id, status)
+ def get_access_key_last_used(self, access_key_id):
+ access_keys_list = self.get_all_access_keys_for_all_users()
+ for key in access_keys_list:
+ if key.access_key_id == access_key_id:
+ return {
+ 'user_name': key.user_name,
+ 'last_used': key.last_used
+ }
+ else:
+ raise IAMNotFoundException(
+ "The Access Key with id {0} cannot be found".format(access_key_id))
+
+ def get_all_access_keys_for_all_users(self):
+ access_keys_list = []
+ for user_name in self.users:
+ access_keys_list += self.get_all_access_keys(user_name)
+ return access_keys_list
+
def get_all_access_keys(self, user_name, marker=None, max_items=None):
user = self.get_user(user_name)
keys = user.get_all_access_keys()
diff --git a/moto/iam/responses.py b/moto/iam/responses.py
index 471da367f..804386cc3 100644
--- a/moto/iam/responses.py
+++ b/moto/iam/responses.py
@@ -454,9 +454,14 @@ class IamResponse(BaseResponse):
template = self.response_template(GENERIC_EMPTY_TEMPLATE)
return template.render(name='UpdateAccessKey')
+ def get_access_key_last_used(self):
+ access_key_id = self._get_param('AccessKeyId')
+ last_used_response = iam_backend.get_access_key_last_used(access_key_id)
+ template = self.response_template(GET_ACCESS_KEY_LAST_USED_TEMPLATE)
+ return template.render(user_name=last_used_response["user_name"], last_used=last_used_response["last_used"])
+
def list_access_keys(self):
user_name = self._get_param('UserName')
-
keys = iam_backend.get_all_access_keys(user_name)
template = self.response_template(LIST_ACCESS_KEYS_TEMPLATE)
return template.render(user_name=user_name, keys=keys)
@@ -1308,6 +1313,18 @@ LIST_ACCESS_KEYS_TEMPLATE = """
"""
+
+GET_ACCESS_KEY_LAST_USED_TEMPLATE = """
+
+
+ {{ user_name }}
+
+ {{ last_used }}
+
+
+
+"""
+
CREDENTIAL_REPORT_GENERATING = """
diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py
index 83edc7f78..7434be754 100644
--- a/tests/test_iam/test_iam.py
+++ b/tests/test_iam/test_iam.py
@@ -13,6 +13,7 @@ from moto.iam.models import aws_managed_policies
from nose.tools import assert_raises, assert_equals
from nose.tools import raises
+from datetime import datetime
from tests.helpers import requires_boto_gte
@@ -735,6 +736,24 @@ def test_update_access_key():
resp['AccessKeyMetadata'][0]['Status'].should.equal('Inactive')
+@mock_iam
+def test_get_access_key_last_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']
+ 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["UserName"].should.equal(create_key_response["UserName"])
+
+
@mock_iam
def test_get_account_authorization_details():
import json