Merge "Skip more security group tests until bug 1182384 is fixed"
diff --git a/doc/source/index.rst b/doc/source/index.rst
index f012097..00c4e9a 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -33,6 +33,13 @@
    field_guide/thirdparty
    field_guide/whitebox
 
+------------------
+API and test cases
+------------------
+.. toctree::
+   :maxdepth: 1
+
+   api/modules
 
 ==================
 Indices and tables
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 4f433a6..12a57db 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -267,6 +267,8 @@
 # Number of seconds to wait while looping to check the status of a
 # container to container synchronization
 container_sync_interval = 5
+# Set to True if the Account Quota middleware is enabled
+accounts_quotas_available = True
 
 [boto]
 # This section contains configuration options used when executing tests
diff --git a/setup.cfg b/setup.cfg
index 3b13b60..7cfc4ce 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -35,3 +35,6 @@
 #                coverage http://pypi.python.org/pypi/coverage
 #                openstack-nose https://github.com/openstack-dev/openstack-nose
 verbosity=2
+
+[pbr]
+autodoc_tree_index_modules=true
diff --git a/tempest/api/compute/images/test_images_oneserver.py b/tempest/api/compute/images/test_images_oneserver.py
index 64f1854..14eced2 100644
--- a/tempest/api/compute/images/test_images_oneserver.py
+++ b/tempest/api/compute/images/test_images_oneserver.py
@@ -89,23 +89,6 @@
         self.assertRaises(exceptions.BadRequest, self.client.create_image,
                           self.server['id'], snapshot_name, meta)
 
-    @testtools.skipUnless(compute.MULTI_USER,
-                          'Need multiple users for this test.')
-    @attr(type=['negative', 'gate'])
-    def test_delete_image_of_another_tenant(self):
-        # Return an error while trying to delete another tenant's image
-        self.servers_client.wait_for_server_status(self.server['id'], 'ACTIVE')
-        snapshot_name = rand_name('test-snap-')
-        resp, body = self.client.create_image(self.server['id'], snapshot_name)
-        image_id = parse_image_id(resp['location'])
-        self.image_ids.append(image_id)
-        self.client.wait_for_image_resp_code(image_id, 200)
-        self.client.wait_for_image_status(image_id, 'ACTIVE')
-
-        # Delete image
-        self.assertRaises(exceptions.NotFound,
-                          self.alt_client.delete_image, image_id)
-
     def _get_default_flavor_disk_size(self, flavor_id):
         resp, flavor = self.flavors_client.get_flavor_details(flavor_id)
         return flavor['disk']
@@ -144,16 +127,6 @@
         self.assertEqual('204', resp['status'])
         self.client.wait_for_resource_deletion(image_id)
 
-    @testtools.skipUnless(compute.MULTI_USER,
-                          'Need multiple users for this test.')
-    @attr(type=['negative', 'gate'])
-    def test_create_image_for_server_in_another_tenant(self):
-        # Creating image of another tenant's server should be return error
-
-        snapshot_name = rand_name('test-snap-')
-        self.assertRaises(exceptions.NotFound, self.alt_client.create_image,
-                          self.server['id'], snapshot_name)
-
     @attr(type=['negative', 'gate'])
     def test_create_second_image_when_first_image_is_being_saved(self):
         # Disallow creating another image when first image is being saved
diff --git a/tempest/api/object_storage/test_account_quotas.py b/tempest/api/object_storage/test_account_quotas.py
new file mode 100644
index 0000000..bc050dc
--- /dev/null
+++ b/tempest/api/object_storage/test_account_quotas.py
@@ -0,0 +1,115 @@
+# Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
+#
+# Author: Joe H. Rahme <joe.hakim.rahme@enovance.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import testtools
+
+from tempest.api.object_storage import base
+from tempest import clients
+from tempest.common.utils.data_utils import arbitrary_string
+from tempest.common.utils.data_utils import rand_name
+import tempest.config
+from tempest import exceptions
+from tempest.test import attr
+
+
+class AccountQuotasTest(base.BaseObjectTest):
+    accounts_quotas_available = \
+        tempest.config.TempestConfig().object_storage.accounts_quotas_available
+
+    @classmethod
+    def setUpClass(cls):
+        super(AccountQuotasTest, cls).setUpClass()
+        cls.container_name = rand_name(name="TestContainer")
+        cls.container_client.create_container(cls.container_name)
+
+        cls.data.setup_test_user()
+
+        cls.os_reselleradmin = clients.Manager(
+            cls.data.test_user,
+            cls.data.test_password,
+            cls.data.test_tenant)
+
+        # Retrieve the ResellerAdmin role id
+        reseller_role_id = None
+        try:
+            _, roles = cls.os_admin.identity_client.list_roles()
+            reseller_role_id = next(r['id'] for r in roles if r['name']
+                                    == 'ResellerAdmin')
+        except StopIteration:
+            msg = "No ResellerAdmin role found"
+            raise exceptions.NotFound(msg)
+
+        # Retrieve the ResellerAdmin tenant id
+        _, users = cls.os_admin.identity_client.get_users()
+        reseller_user_id = next(usr['id'] for usr in users if usr['name']
+                                == cls.data.test_user)
+
+        # Retrieve the ResellerAdmin tenant id
+        _, tenants = cls.os_admin.identity_client.list_tenants()
+        reseller_tenant_id = next(tnt['id'] for tnt in tenants if tnt['name']
+                                  == cls.data.test_tenant)
+
+        # Assign the newly created user the appropriate ResellerAdmin role
+        cls.os_admin.identity_client.assign_user_role(
+            reseller_tenant_id,
+            reseller_user_id,
+            reseller_role_id)
+
+        # Retrieve a ResellerAdmin auth token and use it to set a quota
+        # on the client's account
+        cls.reselleradmin_token = cls.token_client.get_token(
+            cls.data.test_user,
+            cls.data.test_password,
+            cls.data.test_tenant)
+
+        headers = {"X-Auth-Token": cls.reselleradmin_token,
+                   "X-Account-Meta-Quota-Bytes": "20"}
+
+        cls.os.custom_account_client.request("POST", "", headers, "")
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.delete_containers([cls.container_name])
+        cls.data.teardown_all()
+
+        # remove the quota from the container
+        headers = {"X-Auth-Token": cls.reselleradmin_token,
+                   "X-Remove-Account-Meta-Quota-Bytes": "x"}
+
+        cls.os.custom_account_client.request("POST", "", headers, "")
+
+        super(AccountQuotasTest, cls).tearDownClass()
+
+    @testtools.skipIf(not accounts_quotas_available,
+                      "Account Quotas middleware not available")
+    @attr(type="smoke")
+    def test_upload_valid_object(self):
+        object_name = rand_name(name="TestObject")
+        data = arbitrary_string()
+        resp, _ = self.object_client.create_object(self.container_name,
+                                                   object_name, data)
+
+        self.assertEqual(resp["status"], "201")
+
+    @testtools.skipIf(not accounts_quotas_available,
+                      "Account Quotas middleware not available")
+    @attr(type=["negative", "smoke"])
+    def test_upload_large_object(self):
+        object_name = rand_name(name="TestObject")
+        data = arbitrary_string(30)
+        self.assertRaises(exceptions.OverLimit,
+                          self.object_client.create_object,
+                          self.container_name, object_name, data)
diff --git a/tempest/api/object_storage/test_container_acl.py b/tempest/api/object_storage/test_container_acl.py
new file mode 100644
index 0000000..1a31b91
--- /dev/null
+++ b/tempest/api/object_storage/test_container_acl.py
@@ -0,0 +1,225 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.api.object_storage import base
+from tempest.common.utils.data_utils import rand_name
+from tempest import exceptions
+from tempest.test import attr
+from tempest.test import HTTP_SUCCESS
+
+
+class ObjectTestACLs(base.BaseObjectTest):
+    @classmethod
+    def setUpClass(cls):
+        super(ObjectTestACLs, cls).setUpClass()
+        cls.data.setup_test_user()
+        cls.new_token = cls.token_client.get_token(cls.data.test_user,
+                                                   cls.data.test_password,
+                                                   cls.data.test_tenant)
+        cls.custom_headers = {'X-Auth-Token': cls.new_token}
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.data.teardown_all()
+        super(ObjectTestACLs, cls).tearDownClass()
+
+    def setUp(self):
+        super(ObjectTestACLs, self).setUp()
+        self.container_name = rand_name(name='TestContainer')
+        self.container_client.create_container(self.container_name)
+
+    def tearDown(self):
+        self.delete_containers([self.container_name])
+        super(ObjectTestACLs, self).tearDown()
+
+    @attr(type=['negative', 'gate'])
+    def test_write_object_without_using_creds(self):
+        # trying to create object with empty headers
+        # X-Auth-Token is not provided
+        object_name = rand_name(name='Object')
+        self.assertRaises(exceptions.Unauthorized,
+                          self.custom_object_client.create_object,
+                          self.container_name, object_name, 'data')
+
+    @attr(type=['negative', 'gate'])
+    def test_delete_object_without_using_creds(self):
+        # create object
+        object_name = rand_name(name='Object')
+        resp, _ = self.object_client.create_object(self.container_name,
+                                                   object_name, 'data')
+        # trying to delete object with empty headers
+        # X-Auth-Token is not provided
+        self.assertRaises(exceptions.Unauthorized,
+                          self.custom_object_client.delete_object,
+                          self.container_name, object_name)
+
+    @attr(type=['negative', 'gate'])
+    def test_write_object_with_non_authorized_user(self):
+        # attempt to upload another file using non-authorized user
+        # User provided token is forbidden. ACL are not set
+        object_name = rand_name(name='Object')
+        # trying to create object with non-authorized user
+        self.assertRaises(exceptions.Unauthorized,
+                          self.custom_object_client.create_object,
+                          self.container_name, object_name, 'data',
+                          metadata=self.custom_headers)
+
+    @attr(type=['negative', 'gate'])
+    def test_read_object_with_non_authorized_user(self):
+        # attempt to read object using non-authorized user
+        # User provided token is forbidden. ACL are not set
+        object_name = rand_name(name='Object')
+        resp, _ = self.object_client.create_object(
+            self.container_name, object_name, 'data')
+        self.assertEqual(resp['status'], '201')
+        # trying to get object with non authorized user token
+        self.assertRaises(exceptions.Unauthorized,
+                          self.custom_object_client.get_object,
+                          self.container_name, object_name,
+                          metadata=self.custom_headers)
+
+    @attr(type=['negative', 'gate'])
+    def test_delete_object_with_non_authorized_user(self):
+        # attempt to delete object using non-authorized user
+        # User provided token is forbidden. ACL are not set
+        object_name = rand_name(name='Object')
+        resp, _ = self.object_client.create_object(
+            self.container_name, object_name, 'data')
+        self.assertEqual(resp['status'], '201')
+        # trying to delete object with non-authorized user token
+        self.assertRaises(exceptions.Unauthorized,
+                          self.custom_object_client.delete_object,
+                          self.container_name, object_name,
+                          metadata=self.custom_headers)
+
+    @attr(type=['negative', 'smoke'])
+    def test_read_object_without_rights(self):
+        # attempt to read object using non-authorized user
+        # update X-Container-Read metadata ACL
+        cont_headers = {'X-Container-Read': 'badtenant:baduser'}
+        resp_meta, body = self.container_client.update_container_metadata(
+            self.container_name, metadata=cont_headers,
+            metadata_prefix='')
+        self.assertIn(int(resp_meta['status']), HTTP_SUCCESS)
+        # create object
+        object_name = rand_name(name='Object')
+        resp, _ = self.object_client.create_object(self.container_name,
+                                                   object_name, 'data')
+        self.assertEqual(resp['status'], '201')
+        # Trying to read the object without rights
+        self.assertRaises(exceptions.Unauthorized,
+                          self.custom_object_client.get_object,
+                          self.container_name, object_name,
+                          metadata=self.custom_headers)
+
+    @attr(type=['negative', 'smoke'])
+    def test_write_object_without_rights(self):
+        # attempt to write object using non-authorized user
+        # update X-Container-Write metadata ACL
+        cont_headers = {'X-Container-Write': 'badtenant:baduser'}
+        resp_meta, body = self.container_client.update_container_metadata(
+            self.container_name, metadata=cont_headers,
+            metadata_prefix='')
+        self.assertIn(int(resp_meta['status']), HTTP_SUCCESS)
+        # Trying to write the object without rights
+        object_name = rand_name(name='Object')
+        self.assertRaises(exceptions.Unauthorized,
+                          self.custom_object_client.create_object,
+                          self.container_name,
+                          object_name, 'data',
+                          metadata=self.custom_headers)
+
+    @attr(type='smoke')
+    def test_read_object_with_rights(self):
+        # attempt to read object using authorized user
+        # update X-Container-Read metadata ACL
+        cont_headers = {'X-Container-Read':
+                        self.data.test_tenant + ':' + self.data.test_user}
+        resp_meta, body = self.container_client.update_container_metadata(
+            self.container_name, metadata=cont_headers,
+            metadata_prefix='')
+        self.assertIn(int(resp_meta['status']), HTTP_SUCCESS)
+        # create object
+        object_name = rand_name(name='Object')
+        resp, _ = self.object_client.create_object(self.container_name,
+                                                   object_name, 'data')
+        self.assertEqual(resp['status'], '201')
+        # Trying to read the object with rights
+        resp, _ = self.custom_object_client.get_object(
+            self.container_name, object_name,
+            metadata=self.custom_headers)
+        self.assertIn(int(resp['status']), HTTP_SUCCESS)
+
+    @attr(type='smoke')
+    def test_write_object_with_rights(self):
+        # attempt to write object using authorized user
+        # update X-Container-Write metadata ACL
+        cont_headers = {'X-Container-Write':
+                        self.data.test_tenant + ':' + self.data.test_user}
+        resp_meta, body = self.container_client.update_container_metadata(
+            self.container_name, metadata=cont_headers,
+            metadata_prefix='')
+        self.assertIn(int(resp_meta['status']), HTTP_SUCCESS)
+        # Trying to write the object with rights
+        object_name = rand_name(name='Object')
+        resp, _ = self.custom_object_client.create_object(
+            self.container_name,
+            object_name, 'data',
+            metadata=self.custom_headers)
+        self.assertIn(int(resp['status']), HTTP_SUCCESS)
+
+    @attr(type=['negative', 'smoke'])
+    def test_write_object_without_write_rights(self):
+        # attempt to write object using non-authorized user
+        # update X-Container-Read and X-Container-Write metadata ACL
+        cont_headers = {'X-Container-Read':
+                        self.data.test_tenant + ':' + self.data.test_user,
+                        'X-Container-Write': ''}
+        resp_meta, body = self.container_client.update_container_metadata(
+            self.container_name, metadata=cont_headers,
+            metadata_prefix='')
+        self.assertIn(int(resp_meta['status']), HTTP_SUCCESS)
+        # Trying to write the object without write rights
+        object_name = rand_name(name='Object')
+        self.assertRaises(exceptions.Unauthorized,
+                          self.custom_object_client.create_object,
+                          self.container_name,
+                          object_name, 'data',
+                          metadata=self.custom_headers)
+
+    @attr(type=['negative', 'smoke'])
+    def test_delete_object_without_write_rights(self):
+        # attempt to delete object using non-authorized user
+        # update X-Container-Read and X-Container-Write metadata ACL
+        cont_headers = {'X-Container-Read':
+                        self.data.test_tenant + ':' + self.data.test_user,
+                        'X-Container-Write': ''}
+        resp_meta, body = self.container_client.update_container_metadata(
+            self.container_name, metadata=cont_headers,
+            metadata_prefix='')
+        self.assertIn(int(resp_meta['status']), HTTP_SUCCESS)
+        # create object
+        object_name = rand_name(name='Object')
+        resp, _ = self.object_client.create_object(self.container_name,
+                                                   object_name, 'data')
+        self.assertEqual(resp['status'], '201')
+        # Trying to delete the object without write rights
+        self.assertRaises(exceptions.Unauthorized,
+                          self.custom_object_client.delete_object,
+                          self.container_name,
+                          object_name,
+                          metadata=self.custom_headers)
diff --git a/tempest/api/object_storage/test_object_services.py b/tempest/api/object_storage/test_object_services.py
index 6136216..c8d9965 100644
--- a/tempest/api/object_storage/test_object_services.py
+++ b/tempest/api/object_storage/test_object_services.py
@@ -21,7 +21,6 @@
 from tempest.api.object_storage import base
 from tempest.common.utils.data_utils import arbitrary_string
 from tempest.common.utils.data_utils import rand_name
-from tempest import exceptions
 from tempest.test import attr
 from tempest.test import HTTP_SUCCESS
 
@@ -230,74 +229,6 @@
             self.fail("Got exception :%s ; while copying"
                       " object across containers" % e)
 
-    @attr(type=['negative', 'gate'])
-    def test_write_object_without_using_creds(self):
-        # trying to create object with empty headers
-        object_name = rand_name(name='Object')
-        data = arbitrary_string(size=len(object_name),
-                                base_text=object_name)
-        obj_headers = {'Content-Type': 'application/json',
-                       'Accept': 'application/json'}
-        self.assertRaises(exceptions.Unauthorized,
-                          self.custom_object_client.create_object,
-                          self.container_name, object_name, data,
-                          metadata=obj_headers)
-
-    @attr(type=['negative', 'gate'])
-    def test_delete_object_without_using_creds(self):
-        # create object
-        object_name = rand_name(name='Object')
-        data = arbitrary_string(size=len(object_name),
-                                base_text=object_name)
-        resp, _ = self.object_client.create_object(self.container_name,
-                                                   object_name, data)
-        # trying to delete object with empty headers
-        self.assertRaises(exceptions.Unauthorized,
-                          self.custom_object_client.delete_object,
-                          self.container_name, object_name)
-
-    @attr(type=['negative', 'gate'])
-    def test_write_object_with_non_authorized_user(self):
-        # attempt to upload another file using non-authorized user
-        object_name = rand_name(name='Object')
-        data = arbitrary_string(size=len(object_name) * 5,
-                                base_text=object_name)
-
-        # trying to create object with non-authorized user
-        self.assertRaises(exceptions.Unauthorized,
-                          self.custom_object_client.create_object,
-                          self.container_name, object_name, data,
-                          metadata=self.custom_headers)
-
-    @attr(type=['negative', 'gate'])
-    def test_read_object_with_non_authorized_user(self):
-        object_name = rand_name(name='Object')
-        data = arbitrary_string(size=len(object_name) * 5,
-                                base_text=object_name)
-        resp, body = self.object_client.create_object(
-            self.container_name, object_name, data)
-        self.assertEqual(resp['status'], '201')
-
-        # trying to get object with non authorized user token
-        self.assertRaises(exceptions.Unauthorized,
-                          self.custom_object_client.get_object,
-                          self.container_name, object_name,
-                          metadata=self.custom_headers)
-
-    @attr(type=['negative', 'gate'])
-    def test_delete_object_with_non_authorized_user(self):
-        object_name = rand_name(name='Object')
-        data = arbitrary_string(size=len(object_name) * 5,
-                                base_text=object_name)
-        resp, body = self.object_client.create_object(
-            self.container_name, object_name, data)
-        self.assertEqual(resp['status'], '201')
-        # trying to delete object with non-authorized user token
-        self.assertRaises(exceptions.Unauthorized,
-                          self.custom_object_client.delete_object,
-                          self.container_name, object_name,
-                          metadata=self.custom_headers)
-
     @attr(type='gate')
     def test_get_object_using_temp_url(self):
         # access object using temporary URL within expiration time
diff --git a/tempest/api/orchestration/stacks/test_instance_cfn_init.py b/tempest/api/orchestration/stacks/test_instance_cfn_init.py
index 7897b70..fe55ecf 100644
--- a/tempest/api/orchestration/stacks/test_instance_cfn_init.py
+++ b/tempest/api/orchestration/stacks/test_instance_cfn_init.py
@@ -145,7 +145,7 @@
                 'ImageId': cls.orchestration_cfg.image_ref
             })
 
-    @attr(type='gate')
+    @attr(type='slow')
     @testtools.skipIf(existing_keypair, 'Server ssh tests are disabled.')
     def test_can_log_into_created_server(self):
 
@@ -168,7 +168,7 @@
             server, 'ec2-user', pkey=self.keypair['private_key'])
         self.assertTrue(linux_client.can_authenticate())
 
-    @attr(type='gate')
+    @attr(type='slow')
     def test_stack_wait_condition_data(self):
 
         sid = self.stack_identifier
diff --git a/tempest/config.py b/tempest/config.py
index 19170ae..9b1a91e 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -366,6 +366,9 @@
                default=5,
                help="Number of seconds to wait while looping to check the"
                     "status of a container to container synchronization"),
+    cfg.BoolOpt('accounts_quotas_available',
+                default=True,
+                help="Set to True if the Account Quota middleware is enabled"),
 ]