diff --git a/moto/iam/models.py b/moto/iam/models.py
index ebb25b66d..5468e0805 100644
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -4,7 +4,7 @@ from boto.exception import BotoServerError
from moto.core import BaseBackend
from .utils import random_access_key, random_alphanumeric, random_resource_id
from datetime import datetime
-
+import base64
class Role(object):
@@ -135,7 +135,7 @@ class User(object):
datetime.utcnow(),
"%Y-%m-%d-%H-%M-%S"
)
-
+ self.arn = 'arn:aws:iam::123456789012:user/{0}'.format(name)
self.policies = {}
self.access_keys = []
self.password = None
@@ -184,6 +184,45 @@ class User(object):
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"')
raise UnformattedGetAttTemplateException()
+ def to_csv(self):
+ date_format = '%Y-%m-%dT%H:%M:%S+00:00'
+ date_created = datetime.strptime(self.created, '%Y-%m-%d-%H-%M-%S')
+ # aagrawal,arn:aws:iam::509284790694:user/aagrawal,2014-09-01T22:28:48+00:00,true,2014-11-12T23:36:49+00:00,2014-09-03T18:59:00+00:00,N/A,false,true,2014-09-01T22:28:48+00:00,false,N/A,false,N/A,false,N/A
+ if not self.password:
+ password_enabled = 'false'
+ password_last_used = 'not_supported'
+ else:
+ password_enabled = 'true'
+ password_last_used = 'no_information'
+
+ if len(self.access_keys) == 0:
+ access_key_1_active = 'false'
+ access_key_1_last_rotated = 'N/A'
+ access_key_2_active = 'false'
+ access_key_2_last_rotated = '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_2_active = 'false'
+ access_key_2_last_rotated = '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)
+
+ return '{0},{1},{2},{3},{4},{5},not_supported,false,{6},{7},{8},{9},false,N/A,false,N/A'.format(self.name,
+ self.arn,
+ date_created.strftime(date_format),
+ password_enabled,
+ password_last_used,
+ date_created.strftime(date_format),
+ access_key_1_active,
+ access_key_1_last_rotated,
+ access_key_2_active,
+ access_key_2_last_rotated
+ )
+
class IAMBackend(BaseBackend):
@@ -193,6 +232,7 @@ class IAMBackend(BaseBackend):
self.certificates = {}
self.groups = {}
self.users = {}
+ self.credential_report = None
super(IAMBackend, self).__init__()
def create_role(self, role_name, assume_role_policy_document, path):
@@ -394,5 +434,18 @@ class IAMBackend(BaseBackend):
except KeyError:
raise BotoServerError(404, 'Not Found')
+ def report_generated(self):
+ return self.credential_report
+
+ def generate_report(self):
+ self.credential_report = True
+
+ def get_credential_report(self):
+ if not self.credential_report:
+ raise BotoServerError(410, 'ReportNotPresent')
+ 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'
+ for user in self.users:
+ report += self.users[user].to_csv()
+ return base64.b64encode(report.encode('ascii')).decode('ascii')
iam_backend = IAMBackend()
diff --git a/moto/iam/responses.py b/moto/iam/responses.py
index e95494ce4..78030f288 100644
--- a/moto/iam/responses.py
+++ b/moto/iam/responses.py
@@ -219,6 +219,18 @@ class IamResponse(BaseResponse):
template = self.response_template(GENERIC_EMPTY_TEMPLATE)
return template.render(name='DeleteUser')
+ def generate_credential_report(self):
+ if iam_backend.report_generated():
+ template = self.response_template(CREDENTIAL_REPORT_GENERATED)
+ else:
+ template = self.response_template(CREDENTIAL_REPORT_GENERATING)
+ iam_backend.generate_report()
+ return template.render()
+
+ def get_credential_report(self):
+ report = iam_backend.get_credential_report()
+ template = self.response_template(CREDENTIAL_REPORT)
+ return template.render(report=report)
GENERIC_EMPTY_TEMPLATE = """<{{ name }}Response>
@@ -559,3 +571,34 @@ LIST_ACCESS_KEYS_TEMPLATE = """
7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
"""
+
+CREDENTIAL_REPORT_GENERATING = """
+
+
+ STARTED
+ No report exists. Starting a new report generation task
+
+
+ fa788a82-aa8a-11e4-a278-1786c418872b"
+
+"""
+
+CREDENTIAL_REPORT_GENERATED = """
+
+ COMPLETE
+
+
+ fa788a82-aa8a-11e4-a278-1786c418872b"
+
+"""
+
+CREDENTIAL_REPORT = """
+
+ {{ report }}
+ 2015-02-02T20:02:02Z
+ text/csv
+
+
+ fa788a82-aa8a-11e4-a278-1786c418872b"
+
+"""
\ No newline at end of file
diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py
index 11863d83d..454703fec 100644
--- a/tests/test_iam/test_iam.py
+++ b/tests/test_iam/test_iam.py
@@ -1,10 +1,11 @@
from __future__ import unicode_literals
import boto
import sure # noqa
+import re
from nose.tools import assert_raises, assert_equals, assert_not_equals
from boto.exception import BotoServerError
-
+import base64
from moto import mock_iam
@@ -200,3 +201,24 @@ def test_delete_user():
conn.delete_user('my-user')
conn.create_user('my-user')
conn.delete_user('my-user')
+
+@mock_iam()
+def test_generate_credential_report():
+ conn = boto.connect_iam()
+ result = conn.generate_credential_report()
+ result['generate_credential_report_response']['generate_credential_report_result']['state'].should.equal('STARTED')
+ result = conn.generate_credential_report()
+ result['generate_credential_report_response']['generate_credential_report_result']['state'].should.equal('COMPLETE')
+
+@mock_iam()
+def test_get_credential_report():
+ conn = boto.connect_iam()
+ conn.create_user('my-user')
+ with assert_raises(BotoServerError):
+ conn.get_credential_report()
+ result = conn.generate_credential_report()
+ while result['generate_credential_report_response']['generate_credential_report_result']['state'] != 'COMPLETE':
+ result = conn.generate_credential_report()
+ result = conn.get_credential_report()
+ report = base64.b64decode(result['get_credential_report_response']['get_credential_report_result']['content'].encode('ascii')).decode('ascii')
+ report.should.match(r'.*my-user.*')
\ No newline at end of file