]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Implement simple AccessPolicy Resource
authorSteven Hardy <shardy@redhat.com>
Tue, 5 Feb 2013 14:07:14 +0000 (14:07 +0000)
committerSteven Hardy <shardy@redhat.com>
Fri, 8 Feb 2013 17:18:55 +0000 (17:18 +0000)
Implement a simple AccessPolicy resource, which can be used
to restrict in-instance users to specific resources when they
call the DescribeStackResource API action

Fixes bug 1115758

Signed-off-by: Steven Hardy <shardy@redhat.com>
Change-Id: Idc98531388e535ce16308fd5aab5ceecda1de682

heat/api/aws/exception.py
heat/engine/resources/user.py
heat/engine/service.py
heat/tests/test_engine_service.py
heat/tests/test_user.py
templates/WordPress_Single_Instance_With_HA_AccessPolicy.template [new file with mode: 0644]

index 14c8258af07c97a715399ac66a70b76ceaa7c810..2a58212f17c459bb06e8aa71ee14b9c573adf711 100644 (file)
@@ -254,9 +254,12 @@ def map_remote_error(ex):
             'WatchRuleNotFound',
             'StackExists',
         )
+        denied_errors = ('Forbidden', 'NotAuthorized')
 
         if ex.exc_type in inval_param_errors:
             return HeatInvalidParameterValueError(detail=ex.value)
+        elif ex.exc_type in denied_errors:
+            return HeatAccessDeniedError(detail=ex.value)
         else:
             # Map everything else to internal server error for now
             return HeatInternalFailureError(detail=ex.value)
index 5eb23638059266b908e00c194e6d8642ebd0b46f..226a639b98a9fca2af30535f91fa1d2dc9226087 100644 (file)
@@ -21,9 +21,9 @@ from heat.openstack.common import log as logging
 logger = logging.getLogger(__name__)
 
 #
-# We are ignoring Policies and Groups as keystone does not support them.
-#
-# For now support users and accesskeys.
+# We are ignoring Groups as keystone does not support them.
+# For now support users and accesskeys,
+# We also now support a limited heat-native Policy implementation
 #
 
 
@@ -39,12 +39,46 @@ class User(resource.Resource):
     def __init__(self, name, json_snippet, stack):
         super(User, self).__init__(name, json_snippet, stack)
 
+    def _validate_policies(self, policies):
+        for policy in policies:
+            # When we support AWS IAM style policies, we will have to accept
+            # either a ref to an AWS::IAM::Policy defined in the stack, or
+            # and embedded dict describing the policy directly, but for now
+            # we only expect this list to contain strings, which must map
+            # to an OS::Heat::AccessPolicy in this stack
+            # If a non-string (e.g embedded IAM dict policy) is passed, we
+            # ignore the policy (don't reject it because we previously ignored
+            # and we don't want to break templates which previously worked
+            if not isinstance(policy, basestring):
+                logger.warning("Ignoring policy %s, " % policy
+                               + "must be string resource name")
+                continue
+
+            try:
+                policy_rsrc = self.stack.resources[policy]
+            except KeyError:
+                logger.error("Policy %s does not exist in stack %s" %
+                             (policy, self.stack.name))
+                return False
+
+            if not callable(getattr(policy_rsrc, 'access_allowed', None)):
+                logger.error("Policy %s is not an AccessPolicy resource" %
+                             policy)
+                return False
+
+        return True
+
     def handle_create(self):
         passwd = ''
         if self.properties['LoginProfile'] and \
                 'Password' in self.properties['LoginProfile']:
                 passwd = self.properties['LoginProfile']['Password']
 
+        if self.properties['Policies']:
+            if not self._validate_policies(self.properties['Policies']):
+                raise exception.InvalidTemplateAttribute(resource=self.name,
+                                                         key='Policies')
+
         uid = self.keystone().create_stack_user(self.physical_resource_name(),
                                                 passwd)
         self.resource_id_set(uid)
@@ -66,6 +100,18 @@ class User(resource.Resource):
         raise exception.InvalidTemplateAttribute(
             resource=self.physical_resource_name(), key=key)
 
+    def access_allowed(self, resource_name):
+        policies = self.properties['Policies']
+        for policy in policies:
+            if not isinstance(policy, basestring):
+                logger.warning("Ignoring policy %s, " % policy
+                               + "must be string resource name")
+                continue
+            policy_rsrc = self.stack.resources[policy]
+            if not policy_rsrc.access_allowed(resource_name):
+                return False
+        return True
+
 
 class AccessKey(resource.Resource):
     properties_schema = {'Serial': {'Type': 'Integer',
@@ -80,7 +126,7 @@ class AccessKey(resource.Resource):
         super(AccessKey, self).__init__(name, json_snippet, stack)
         self._secret = None
 
-    def _get_userid(self):
+    def _get_user(self):
         """
         Helper function to derive the keystone userid, which is stored in the
         resource_id of the User associated with this key.  We want to avoid
@@ -95,11 +141,12 @@ class AccessKey(resource.Resource):
         for r in self.stack.resources:
             refid = self.stack.resources[r].FnGetRefId()
             if refid == self.properties['UserName']:
-                return self.stack.resources[r].resource_id
+                return self.stack.resources[r]
 
     def handle_create(self):
-        user_id = self._get_userid()
-        if user_id is None:
+        try:
+            user_id = self._get_user().resource_id
+        except AttributeError:
             raise exception.NotFound('could not find user %s' %
                                      self.properties['UserName'])
 
@@ -117,7 +164,7 @@ class AccessKey(resource.Resource):
     def handle_delete(self):
         self.resource_id_set(None)
         self._secret = None
-        user_id = self._get_userid()
+        user_id = self._get_user().resource_id
         if user_id and self.resource_id:
             self.keystone().delete_ec2_keypair(user_id, self.resource_id)
 
@@ -125,7 +172,7 @@ class AccessKey(resource.Resource):
         '''
         Return the user's access key, fetching it from keystone if necessary
         '''
-        user_id = self._get_userid()
+        user_id = self._get_user().resource_id
         if self._secret is None:
             if not self.resource_id:
                 logger.warn('could not get secret for %s Error:%s' %
@@ -165,9 +212,37 @@ class AccessKey(resource.Resource):
                                              key, log_res))
         return unicode(res)
 
+    def access_allowed(self, resource_name):
+        return self._get_user().access_allowed(resource_name)
+
+
+class AccessPolicy(resource.Resource):
+    properties_schema = {'AllowedResources': {'Type': 'List',
+                                              'Required': True}}
+
+    def __init__(self, name, json_snippet, stack):
+        super(AccessPolicy, self).__init__(name, json_snippet, stack)
+
+    def handle_create(self):
+        resources = self.properties['AllowedResources']
+        # All of the provided resource names must exist in this stack
+        for resource in resources:
+            if resource not in self.stack:
+                logger.error("AccessPolicy resource %s not in stack" %
+                             resource)
+                raise exception.ResourceNotFound(resource_name=resource,
+                                                 stack_name=self.stack.name)
+
+    def handle_update(self, json_snippet):
+        return self.UPDATE_REPLACE
+
+    def access_allowed(self, resource_name):
+        return resource_name in self.properties['AllowedResources']
+
 
 def resource_mapping():
     return {
         'AWS::IAM::User': User,
         'AWS::IAM::AccessKey': AccessKey,
+        'OS::Heat::AccessPolicy': AccessPolicy,
     }
index 1d397986b62bd9c40618d324c620e9becb083b93..66009e91265ea867f9b716df875501736bb2492a 100644 (file)
@@ -15,6 +15,7 @@
 
 import functools
 import webob
+import json
 
 from heat.common import context
 from heat.db import api as db_api
@@ -363,11 +364,51 @@ class EngineService(service.Service):
 
         return [api.format_event(Event.load(context, e.id)) for e in events]
 
+    def _authorize_stack_user(self, context, stack, resource_name):
+        '''
+        Filter access to describe_stack_resource for stack in-instance users
+        - The user must map to a User resource defined in the requested stack
+        - The user resource must validate OK against any Policy specified
+        '''
+        # We're expecting EC2 credentials because all in-instance credentials
+        # are deployed as ec2 keypairs
+        try:
+            ec2_creds = json.loads(context.aws_creds).get('ec2Credentials')
+        except TypeError, AttributeError:
+            ec2_creds = None
+
+        if ec2_creds:
+            access_key = ec2_creds.get('access')
+            # Then we look up the AccessKey resource and check the stack
+            try:
+                akey_rsrc = self.find_physical_resource(context, access_key)
+            except exception.PhysicalResourceNotFound:
+                logger.warning("access_key % not found!" % access_key)
+                return False
+
+            akey_rsrc_id = identifier.ResourceIdentifier(**akey_rsrc)
+            if stack.identifier() == akey_rsrc_id.stack():
+                # The stack matches, so check if access is allowed to this
+                # resource via the AccessKey resource access_allowed()
+                ak_akey_rsrc = stack[akey_rsrc_id.resource_name]
+                return ak_akey_rsrc.access_allowed(resource_name)
+            else:
+                logger.warning("Cannot access resource from wrong stack!")
+        else:
+            logger.warning("Cannot access resource, invalid credentials!")
+
+        return False
+
     @request_context
     def describe_stack_resource(self, context, stack_identity, resource_name):
         s = self._get_stack(context, stack_identity)
-
         stack = parser.Stack.load(context, stack=s)
+
+        if cfg.CONF.heat_stack_user_role in context.roles:
+            if not self._authorize_stack_user(context, stack, resource_name):
+                logger.warning("Access denied to resource %s" % resource_name)
+                raise exception.Forbidden()
+
         if resource_name not in stack:
             raise exception.ResourceNotFound(resource_name=resource_name,
                                              stack_name=stack.name)
index ff9360c6e9e689c91c535882d13c4b3edcae9ec2..45ca0632528f40ee1210afe5941871df3411f078 100644 (file)
@@ -31,6 +31,7 @@ from heat.engine import service
 from heat.engine.resources import instance as instances
 from heat.engine import watchrule
 from heat.openstack.common import threadgroup
+from heat.openstack.common import cfg
 
 
 tests_dir = os.path.dirname(os.path.realpath(__file__))
@@ -368,6 +369,8 @@ class stackServiceTest(unittest.TestCase):
         ctx = create_context(m, cls.username, cls.tenant)
         cls.stack_name = 'service_test_stack'
 
+        cfg.CONF.set_default('heat_stack_user_role', 'stack_user_role')
+
         stack = get_wordpress_stack(cls.stack_name, ctx)
 
         setup_mocks(m, stack)
@@ -591,6 +594,21 @@ class stackServiceTest(unittest.TestCase):
                           self.man.describe_stack_resource,
                           self.ctx, self.stack_identity, 'foo')
 
+    def test_stack_resource_describe_stack_user_deny(self):
+        self.ctx.roles = [cfg.CONF.heat_stack_user_role]
+        self.m.StubOutWithMock(service.EngineService, '_authorize_stack_user')
+        service.EngineService._authorize_stack_user(self.ctx, mox.IgnoreArg(),
+                                                    'foo').AndReturn(False)
+        self.m.ReplayAll()
+        self.assertRaises(exception.Forbidden,
+                          self.man.describe_stack_resource,
+                          self.ctx, self.stack_identity, 'foo')
+
+    def test_stack_authorize_stack_user_nocreds(self):
+        self.assertFalse(self.man._authorize_stack_user(self.ctx,
+                                                        self.stack_identity,
+                                                        'foo'))
+
     def test_stack_resources_describe(self):
         resources = self.man.describe_stack_resources(self.ctx,
                                                       self.stack_identity,
index d65e4320ad963f347a833fa2e80f7e10282f9390..335d741d3254cd2564ec355c6a252df47c5ae5e5 100644 (file)
@@ -41,10 +41,10 @@ class UserTest(unittest.TestCase):
         self.m.UnsetStubs()
         print "UserTest teardown complete"
 
-    def load_template(self):
+    def load_template(self, template_name='Rails_Single_Instance.template'):
         self.path = os.path.dirname(os.path.realpath(__file__)).\
             replace('heat/tests', 'templates')
-        f = open("%s/Rails_Single_Instance.template" % self.path)
+        f = open("%s/%s" % (self.path, template_name))
         t = template_format.parse(f.read())
         f.close()
         return t
@@ -111,6 +111,114 @@ class UserTest(unittest.TestCase):
         self.assertEqual('DELETE_COMPLETE', resource.state)
         self.m.VerifyAll()
 
+    def test_user_validate_policies(self):
+
+        self.m.StubOutWithMock(user.User, 'keystone')
+        user.User.keystone().MultipleTimes().AndReturn(self.fc)
+
+        self.m.ReplayAll()
+
+        tmpl = 'WordPress_Single_Instance_With_HA_AccessPolicy.template'
+        t = self.load_template(template_name=tmpl)
+        stack = self.parse_stack(t)
+
+        resource = self.create_user(t, stack, 'CfnUser')
+        self.assertEqual(self.fc.user_id, resource.resource_id)
+        self.assertEqual('test_stack.CfnUser', resource.FnGetRefId())
+        self.assertEqual('CREATE_COMPLETE', resource.state)
+
+        self.assertEqual([u'WebServerAccessPolicy'],
+                         resource.properties['Policies'])
+
+        # OK
+        self.assertTrue(
+            resource._validate_policies([u'WebServerAccessPolicy']))
+
+        # Resource name doesn't exist in the stack
+        self.assertFalse(resource._validate_policies([u'NoExistAccessPolicy']))
+
+        # Resource name is wrong Resource type
+        self.assertFalse(resource._validate_policies([u'NoExistAccessPolicy',
+                                                      u'WikiDatabase']))
+
+        # Wrong type (AWS embedded policy format, not yet supported)
+        dict_policy = {"PolicyName": "AccessForCFNInit",
+                       "PolicyDocument":
+                       {"Statement": [{"Effect": "Allow",
+                                       "Action":
+                                       "cloudformation:DescribeStackResource",
+                                       "Resource": "*"}]}}
+
+        # However we should just ignore it to avoid breaking existing templates
+        self.assertTrue(resource._validate_policies([dict_policy]))
+
+        self.m.VerifyAll()
+
+    def test_user_create_bad_policies(self):
+        self.m.ReplayAll()
+
+        tmpl = 'WordPress_Single_Instance_With_HA_AccessPolicy.template'
+        t = self.load_template(template_name=tmpl)
+        t['Resources']['CfnUser']['Properties']['Policies'] = ['NoExistBad']
+        stack = self.parse_stack(t)
+        resource_name = 'CfnUser'
+        resource = user.User(resource_name,
+                             t['Resources'][resource_name],
+                             stack)
+        self.assertRaises(exception.InvalidTemplateAttribute,
+                          resource.handle_create)
+        self.m.VerifyAll()
+
+    def test_user_access_allowed(self):
+
+        self.m.StubOutWithMock(user.User, 'keystone')
+        user.User.keystone().MultipleTimes().AndReturn(self.fc)
+
+        self.m.StubOutWithMock(user.AccessPolicy, 'access_allowed')
+        user.AccessPolicy.access_allowed('a_resource').AndReturn(True)
+        user.AccessPolicy.access_allowed('b_resource').AndReturn(False)
+
+        self.m.ReplayAll()
+
+        tmpl = 'WordPress_Single_Instance_With_HA_AccessPolicy.template'
+        t = self.load_template(template_name=tmpl)
+        stack = self.parse_stack(t)
+
+        resource = self.create_user(t, stack, 'CfnUser')
+        self.assertEqual(self.fc.user_id, resource.resource_id)
+        self.assertEqual('test_stack.CfnUser', resource.FnGetRefId())
+        self.assertEqual('CREATE_COMPLETE', resource.state)
+
+        self.assertTrue(resource.access_allowed('a_resource'))
+        self.assertFalse(resource.access_allowed('b_resource'))
+        self.m.VerifyAll()
+
+    def test_user_access_allowed_ignorepolicy(self):
+
+        self.m.StubOutWithMock(user.User, 'keystone')
+        user.User.keystone().MultipleTimes().AndReturn(self.fc)
+
+        self.m.StubOutWithMock(user.AccessPolicy, 'access_allowed')
+        user.AccessPolicy.access_allowed('a_resource').AndReturn(True)
+        user.AccessPolicy.access_allowed('b_resource').AndReturn(False)
+
+        self.m.ReplayAll()
+
+        tmpl = 'WordPress_Single_Instance_With_HA_AccessPolicy.template'
+        t = self.load_template(template_name=tmpl)
+        t['Resources']['CfnUser']['Properties']['Policies'] = [
+            'WebServerAccessPolicy', {'an_ignored': 'policy'}]
+        stack = self.parse_stack(t)
+
+        resource = self.create_user(t, stack, 'CfnUser')
+        self.assertEqual(self.fc.user_id, resource.resource_id)
+        self.assertEqual('test_stack.CfnUser', resource.FnGetRefId())
+        self.assertEqual('CREATE_COMPLETE', resource.state)
+
+        self.assertTrue(resource.access_allowed('a_resource'))
+        self.assertFalse(resource.access_allowed('b_resource'))
+        self.m.VerifyAll()
+
 
 @attr(tag=['unit', 'resource', 'AccessKey'])
 @attr(speed='fast')
@@ -187,13 +295,9 @@ class AccessKeyTest(unittest.TestCase):
         resource._secret = None
         self.assertEqual(resource.FnGetAtt('SecretAccessKey'),
                          self.fc.secret)
-        try:
-            resource.FnGetAtt('Foo')
-        except exception.InvalidTemplateAttribute:
-            pass
-        else:
-            raise Exception('Expected InvalidTemplateAttribute')
 
+        self.assertRaises(exception.InvalidTemplateAttribute,
+                          resource.FnGetAtt, 'Foo')
         self.assertEqual(None, resource.delete())
         self.m.VerifyAll()
 
@@ -216,3 +320,101 @@ class AccessKeyTest(unittest.TestCase):
                          resource.state)
 
         self.m.VerifyAll()
+
+
+@attr(tag=['unit', 'resource', 'AccessPolicy'])
+@attr(speed='fast')
+class AccessPolicyTest(unittest.TestCase):
+    def setUp(self):
+        self.m = mox.Mox()
+        self.fc = fakes.FakeKeystoneClient(username='test_stack.CfnUser')
+        cfg.CONF.set_default('heat_stack_user_role', 'stack_user_role')
+
+    def tearDown(self):
+        self.m.UnsetStubs()
+        print "UserTest teardown complete"
+
+    def load_template(self):
+        template_name =\
+            'WordPress_Single_Instance_With_HA_AccessPolicy.template'
+        self.path = os.path.dirname(os.path.realpath(__file__)).\
+            replace('heat/tests', 'templates')
+        f = open("%s/%s" % (self.path, template_name))
+        t = template_format.parse(f.read())
+        f.close()
+        return t
+
+    def parse_stack(self, t):
+        ctx = context.RequestContext.from_dict({
+            'tenant_id': 'test_tenant',
+            'username': 'test_username',
+            'password': 'password',
+            'auth_url': 'http://localhost:5000/v2.0'})
+        template = parser.Template(t)
+        params = parser.Parameters('test_stack',
+                                   template,
+                                   {'KeyName': 'test',
+                                    'DBRootPassword': 'test',
+                                    'DBUsername': 'test',
+                                    'DBPassword': 'test'})
+        stack = parser.Stack(ctx, 'test_stack', template, params)
+
+        return stack
+
+    def test_accesspolicy_create_ok(self):
+        t = self.load_template()
+        stack = self.parse_stack(t)
+
+        resource_name = 'WebServerAccessPolicy'
+        resource = user.AccessPolicy(resource_name,
+                                     t['Resources'][resource_name],
+                                     stack)
+        self.assertEqual(None, resource.create())
+        self.assertEqual(user.User.CREATE_COMPLETE, resource.state)
+
+    def test_accesspolicy_create_ok_empty(self):
+        t = self.load_template()
+        resource_name = 'WebServerAccessPolicy'
+        t['Resources'][resource_name]['Properties']['AllowedResources'] = []
+        stack = self.parse_stack(t)
+
+        resource = user.AccessPolicy(resource_name,
+                                     t['Resources'][resource_name],
+                                     stack)
+        self.assertEqual(None, resource.create())
+        self.assertEqual(user.User.CREATE_COMPLETE, resource.state)
+
+    def test_accesspolicy_create_err_notfound(self):
+        t = self.load_template()
+        resource_name = 'WebServerAccessPolicy'
+        t['Resources'][resource_name]['Properties']['AllowedResources'] = [
+            'NoExistResource']
+        stack = self.parse_stack(t)
+
+        resource = user.AccessPolicy(resource_name,
+                                     t['Resources'][resource_name],
+                                     stack)
+        self.assertRaises(exception.ResourceNotFound, resource.handle_create)
+
+    def test_accesspolicy_update(self):
+        t = self.load_template()
+        resource_name = 'WebServerAccessPolicy'
+        stack = self.parse_stack(t)
+
+        resource = user.AccessPolicy(resource_name,
+                                     t['Resources'][resource_name],
+                                     stack)
+        self.assertEqual(user.AccessPolicy.UPDATE_REPLACE,
+                         resource.handle_update({}))
+
+    def test_accesspolicy_access_allowed(self):
+        t = self.load_template()
+        resource_name = 'WebServerAccessPolicy'
+        stack = self.parse_stack(t)
+
+        resource = user.AccessPolicy(resource_name,
+                                     t['Resources'][resource_name],
+                                     stack)
+        self.assertTrue(resource.access_allowed('WikiDatabase'))
+        self.assertFalse(resource.access_allowed('NotWikiDatabase'))
+        self.assertFalse(resource.access_allowed(None))
diff --git a/templates/WordPress_Single_Instance_With_HA_AccessPolicy.template b/templates/WordPress_Single_Instance_With_HA_AccessPolicy.template
new file mode 100644 (file)
index 0000000..082d998
--- /dev/null
@@ -0,0 +1,301 @@
+{
+  "AWSTemplateFormatVersion" : "2010-09-09",
+
+  "Description" : "AWS CloudFormation Sample Template WordPress_Multi_Instance: WordPress is web software you can use to create a beautiful website or blog. This template installs two instances: one running a WordPress deployment and the other using a local MySQL database to store the data.",
+
+  "Parameters" : {
+
+    "KeyName" : {
+      "Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instances",
+      "Type" : "String"
+    },
+
+    "InstanceType" : {
+      "Description" : "WebServer EC2 instance type",
+      "Type" : "String",
+      "Default" : "m1.large",
+      "AllowedValues" : [ "t1.micro", "m1.small", "m1.large", "m1.xlarge", "m2.xlarge", "m2.2xlarge", "m2.4xlarge", "c1.medium", "c1.xlarge", "cc1.4xlarge" ],
+      "ConstraintDescription" : "must be a valid EC2 instance type."
+    },
+
+    "DBName": {
+      "Default": "wordpress",
+      "Description" : "The WordPress database name",
+      "Type": "String",
+      "MinLength": "1",
+      "MaxLength": "64",
+      "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*",
+      "ConstraintDescription" : "must begin with a letter and contain only alphanumeric characters."
+    },
+
+    "DBUsername": {
+      "Default": "admin",
+      "NoEcho": "true",
+      "Description" : "The WordPress database admin account username",
+      "Type": "String",
+      "MinLength": "1",
+      "MaxLength": "16",
+      "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*",
+      "ConstraintDescription" : "must begin with a letter and contain only alphanumeric characters."
+    },
+
+    "DBPassword": {
+      "Default": "admin",
+      "NoEcho": "true",
+      "Description" : "The WordPress database admin account password",
+      "Type": "String",
+      "MinLength": "1",
+      "MaxLength": "41",
+      "AllowedPattern" : "[a-zA-Z0-9]*",
+      "ConstraintDescription" : "must contain only alphanumeric characters."
+    },
+
+    "DBRootPassword": {
+      "Default": "admin",
+      "NoEcho": "true",
+      "Description" : "Root password for MySQL",
+      "Type": "String",
+      "MinLength": "1",
+      "MaxLength": "41",
+      "AllowedPattern" : "[a-zA-Z0-9]*",
+      "ConstraintDescription" : "must contain only alphanumeric characters."
+    },
+    "LinuxDistribution": {
+      "Default": "F17",
+      "Description" : "Distribution of choice",
+      "Type": "String",
+      "AllowedValues" : [ "F16", "F17", "U10", "RHEL-6.1", "RHEL-6.2", "RHEL-6.3" ]
+    },
+    "HupPollInterval": {
+      "Default": "1",
+      "Description" : "Interval for cfn-hup",
+      "Type": "String"
+    }
+  },
+
+  "Mappings" : {
+    "AWSInstanceType2Arch" : {
+      "t1.micro"    : { "Arch" : "32" },
+      "m1.small"    : { "Arch" : "32" },
+      "m1.large"    : { "Arch" : "64" },
+      "m1.xlarge"   : { "Arch" : "64" },
+      "m2.xlarge"   : { "Arch" : "64" },
+      "m2.2xlarge"  : { "Arch" : "64" },
+      "m2.4xlarge"  : { "Arch" : "64" },
+      "c1.medium"   : { "Arch" : "32" },
+      "c1.xlarge"   : { "Arch" : "64" },
+      "cc1.4xlarge" : { "Arch" : "64" }
+    },
+    "DistroArch2AMI": {
+      "F16"      : { "32" : "F16-i386-cfntools", "64" : "F16-x86_64-cfntools" },
+      "F17"      : { "32" : "F17-i386-cfntools", "64" : "F17-x86_64-cfntools" },
+      "U10"      : { "32" : "U10-i386-cfntools", "64" : "U10-x86_64-cfntools" },
+      "RHEL-6.1" : { "32" : "rhel61-i386-cfntools", "64" : "rhel61-x86_64-cfntools" },
+      "RHEL-6.2" : { "32" : "rhel62-i386-cfntools", "64" : "rhel62-x86_64-cfntools" },
+      "RHEL-6.3" : { "32" : "rhel63-i386-cfntools", "64" : "rhel63-x86_64-cfntools" }
+    }
+  },
+
+  "Resources" : {
+    "CfnUser" : {
+      "Type" : "AWS::IAM::User",
+      "Properties" : {
+        "Policies" : [ { "Ref": "WebServerAccessPolicy"} ]
+      }
+    },
+    "WebServerAccessPolicy" : {
+      "Type" : "OS::Heat::AccessPolicy",
+      "Properties" : {
+        "AllowedResources" : [ "WikiDatabase" ]
+      }
+    },
+    "WebServerKeys" : {
+      "Type" : "AWS::IAM::AccessKey",
+      "Properties" : {
+        "UserName" : {"Ref": "CfnUser"}
+      }
+    },
+    "WebServerRestartPolicy" : {
+      "Type" : "OS::Heat::HARestarter",
+      "Properties" : {
+        "InstanceId" : { "Ref" : "WikiDatabase" }
+      }
+    },
+    "HttpFailureAlarm": {
+     "Type": "AWS::CloudWatch::Alarm",
+     "Properties": {
+        "AlarmDescription": "Restart the WikiDatabase if httpd fails > 3 times in 10 minutes",
+        "MetricName": "ServiceFailure",
+        "Namespace": "system/linux",
+        "Statistic": "SampleCount",
+        "Period": "300",
+        "EvaluationPeriods": "1",
+        "Threshold": "2",
+        "AlarmActions": [ { "Ref": "WebServerRestartPolicy" } ],
+        "ComparisonOperator": "GreaterThanThreshold"
+      }
+    },
+    "WikiDatabase": {
+      "Type": "AWS::EC2::Instance",
+      "Metadata" : {
+        "AWS::CloudFormation::Init" : {
+          "config" : {
+            "files" : {
+              "/etc/cfn/cfn-credentials" : {
+                "content" : { "Fn::Join" : ["", [
+                  "AWSAccessKeyId=", { "Ref" : "WebServerKeys" }, "\n",
+                  "AWSSecretKey=", {"Fn::GetAtt": ["WebServerKeys",
+                                    "SecretAccessKey"]}, "\n"
+                ]]},
+                "mode"    : "000400",
+                "owner"   : "root",
+                "group"   : "root"
+              },
+
+              "/etc/cfn/cfn-hup.conf" : {
+                "content" : { "Fn::Join" : ["", [
+                  "[main]\n",
+                  "stack=", { "Ref" : "AWS::StackName" }, "\n",
+                  "credential-file=/etc/cfn/cfn-credentials\n",
+                  "region=", { "Ref" : "AWS::Region" }, "\n",
+                  "interval=", { "Ref" : "HupPollInterval" }, "\n"
+                ]]},
+                "mode"    : "000400",
+                "owner"   : "root",
+                "group"   : "root"
+              },
+
+              "/etc/cfn/notify-on-httpd-restarted" : {
+                "content" : { "Fn::Join" : ["", [
+                "#!/bin/sh\n",
+                "/opt/aws/bin/cfn-push-stats --watch ",
+                { "Ref" : "HttpFailureAlarm" },
+                " --service-failure\n"
+                ]]},
+                "mode"    : "000700",
+                "owner"   : "root",
+                "group"   : "root"
+              },
+
+              "/tmp/cfn-hup-crontab.txt" : {
+                "content" : { "Fn::Join" : ["", [
+                "MAIL=\"\"\n",
+                "\n",
+                "* * * * * /opt/aws/bin/cfn-hup -f\n"
+                ]]},
+                "mode"    : "000600",
+                "owner"   : "root",
+                "group"   : "root"
+              },
+
+              "/tmp/setup.mysql" : {
+                "content" : { "Fn::Join" : ["", [
+                "CREATE DATABASE ", { "Ref" : "DBName" }, ";\n",
+                "GRANT ALL PRIVILEGES ON ", { "Ref" : "DBName" },
+                ".* TO '", { "Ref" : "DBUsername" }, "'@'localhost'\n",
+                "IDENTIFIED BY '", { "Ref" : "DBPassword" }, "';\n",
+                "FLUSH PRIVILEGES;\n",
+                "EXIT\n"
+                  ]]},
+                "mode"  : "000644",
+                "owner" : "root",
+                "group" : "root"
+              },
+
+              "/etc/cfn/hooks.conf" : {
+                "content": { "Fn::Join" : ["", [
+                  "[cfn-http-restarted]\n",
+                  "triggers=service.restarted\n",
+                  "path=Resources.WikiDatabase.Metadata\n",
+                  "action=/etc/cfn/notify-on-httpd-restarted\n",
+                  "runas=root\n"
+                ]]},
+                "mode"    : "000400",
+                "owner"   : "root",
+                "group"   : "root"
+              }
+            },
+            "packages" : {
+              "yum" : {
+                "cronie"       : [],
+                "mysql"        : [],
+                "mysql-server" : [],
+                "httpd"        : [],
+                "wordpress"    : []
+              }
+            },
+            "services" : {
+              "systemd" : {
+                "mysqld"   : { "enabled" : "true", "ensureRunning" : "true" },
+                "httpd"    : { "enabled" : "true", "ensureRunning" : "true" },
+                "crond"    : { "enabled" : "true", "ensureRunning" : "true" }
+              }
+            }
+          }
+        }
+      },
+      "Properties": {
+        "ImageId" : { "Fn::FindInMap" : [ "DistroArch2AMI", { "Ref" : "LinuxDistribution" },
+                          { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] },
+        "InstanceType"   : { "Ref" : "InstanceType" },
+        "KeyName"        : { "Ref" : "KeyName" },
+        "UserData"       : { "Fn::Base64" : { "Fn::Join" : ["", [
+          "#!/bin/bash -v\n",
+          "# Helper function\n",
+          "function error_exit\n",
+          "{\n",
+          "  /opt/aws/bin/cfn-signal -e 1 -r \"$1\" '", { "Ref" : "WaitHandle" }, "'\n",
+          "  exit 1\n",
+          "}\n",
+
+          "/opt/aws/bin/cfn-init -s ", { "Ref" : "AWS::StackName" },
+          " -r WikiDatabase ",
+          " --access-key ", { "Ref" : "WebServerKeys" },
+          " --secret-key ", {"Fn::GetAtt": ["WebServerKeys", "SecretAccessKey"]},
+          " --region ", { "Ref" : "AWS::Region" },
+          " || error_exit 'Failed to run cfn-init'\n",
+
+          "# Setup MySQL root password and create a user\n",
+          "mysqladmin -u root password '", { "Ref" : "DBRootPassword" },
+          "' || error_exit 'Failed to initialize root password'\n",
+
+          "mysql -u root --password='", { "Ref" : "DBRootPassword" },
+          "' < /tmp/setup.mysql || error_exit 'Failed to create database.'\n",
+
+          "sed --in-place --e s/database_name_here/", { "Ref" : "DBName" },
+          "/ --e s/username_here/", { "Ref" : "DBUsername" },
+          "/ --e s/password_here/", { "Ref" : "DBPassword" },
+          "/ /usr/share/wordpress/wp-config.php\n",
+
+          "# install cfn-hup crontab\n",
+          "crontab /tmp/cfn-hup-crontab.txt\n",
+
+          "# All is well so signal success\n",
+          "/opt/aws/bin/cfn-signal -e 0 -r \"Wiki server setup complete\" '",
+          { "Ref" : "WaitHandle" }, "'\n"
+        ]]}}
+      }
+    },
+
+    "WaitHandle" : {
+      "Type" : "AWS::CloudFormation::WaitConditionHandle"
+    },
+
+    "WaitCondition" : {
+      "Type" : "AWS::CloudFormation::WaitCondition",
+      "DependsOn" : "WikiDatabase",
+      "Properties" : {
+        "Handle" : {"Ref" : "WaitHandle"},
+        "Count" : "1",
+        "Timeout" : "600"
+      }
+    }
+  },
+
+  "Outputs" : {
+    "WebsiteURL" : {
+      "Value" : { "Fn::Join" : ["", ["http://", { "Fn::GetAtt" : [ "WikiDatabase", "PublicIp" ]}, "/wordpress"]] },
+      "Description" : "URL for Wordpress wiki"
+    }
+  }
+}