Merge "PCI-DSS tests"
diff --git a/tempest/api/identity/admin/v3/test_users.py b/tempest/api/identity/admin/v3/test_users.py
index fd2683e..3ec4ff1 100644
--- a/tempest/api/identity/admin/v3/test_users.py
+++ b/tempest/api/identity/admin/v3/test_users.py
@@ -15,11 +15,17 @@
 
 import time
 
+import testtools
+
 from tempest.api.identity import base
 from tempest.common.utils import data_utils
+from tempest import config
 from tempest import test
 
 
+CONF = config.CONF
+
+
 class UsersV3TestJSON(base.BaseIdentityV3AdminTest):
 
     @test.idempotent_id('b537d090-afb9-4519-b95d-270b0708e87e')
@@ -152,3 +158,30 @@
         user = self.setup_test_user()
         fetched_user = self.users_client.show_user(user['id'])['user']
         self.assertEqual(user['id'], fetched_user['id'])
+
+    @testtools.skipUnless(CONF.identity_feature_enabled.security_compliance,
+                          'Security compliance not available.')
+    @test.idempotent_id('568cd46c-ee6c-4ab4-a33a-d3791931979e')
+    def test_password_history_not_enforced_in_admin_reset(self):
+        old_password = self.os.credentials.password
+        user_id = self.os.credentials.user_id
+
+        new_password = data_utils.rand_password()
+        self.users_client.update_user(user_id, password=new_password)
+        # To be safe, we add this cleanup to restore the original password in
+        # case something goes wrong before it is restored later.
+        self.addCleanup(
+            self.users_client.update_user, user_id, password=old_password)
+
+        # Check authorization with new password
+        self.token.auth(user_id=user_id, password=new_password)
+
+        if CONF.identity.user_unique_last_password_count > 1:
+            # The password history is not enforced via the admin reset route.
+            # We can set the same password.
+            self.users_client.update_user(user_id, password=new_password)
+
+        # Restore original password
+        self.users_client.update_user(user_id, password=old_password)
+        # Check authorization with old password
+        self.token.auth(user_id=user_id, password=old_password)
diff --git a/tempest/api/identity/v2/test_users.py b/tempest/api/identity/v2/test_users.py
index 33d212c..bafb1f2 100644
--- a/tempest/api/identity/v2/test_users.py
+++ b/tempest/api/identity/v2/test_users.py
@@ -16,11 +16,15 @@
 import time
 
 from tempest.api.identity import base
+from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import exceptions
 from tempest import test
 
 
+CONF = config.CONF
+
+
 class IdentityUsersTest(base.BaseIdentityV2Test):
 
     @classmethod
@@ -31,36 +35,10 @@
         cls.password = cls.creds.password
         cls.tenant_name = cls.creds.tenant_name
 
-    @test.idempotent_id('165859c9-277f-4124-9479-a7d1627b0ca7')
-    def test_user_update_own_password(self):
-
-        def _restore_password(client, user_id, old_pass, new_pass):
-            # Reset auth to get a new token with the new password
-            client.auth_provider.clear_auth()
-            client.auth_provider.credentials.password = new_pass
-            client.update_user_own_password(user_id, password=old_pass,
-                                            original_password=new_pass)
-            # Reset auth again to verify the password restore does work.
-            # Clear auth restores the original credentials and deletes
-            # cached auth data
-            client.auth_provider.clear_auth()
-            # NOTE(lbragstad): Fernet tokens are not subsecond aware and
-            # Keystone should only be precise to the second. Sleep to ensure we
-            # are passing the second boundary before attempting to
-            # authenticate.
-            time.sleep(1)
-            client.auth_provider.set_auth()
-
-        old_pass = self.creds.password
-        new_pass = data_utils.rand_password()
-        user_id = self.creds.user_id
-        # to change password back. important for allow_tenant_isolation = false
-        self.addCleanup(_restore_password, self.non_admin_users_client,
-                        user_id, old_pass=old_pass, new_pass=new_pass)
-
-        # user updates own password
+    def _update_password(self, user_id, original_password, password):
         self.non_admin_users_client.update_user_own_password(
-            user_id, password=new_pass, original_password=old_pass)
+            user_id, password=password, original_password=original_password)
+
         # NOTE(morganfainberg): Fernet tokens are not subsecond aware and
         # Keystone should only be precise to the second. Sleep to ensure
         # we are passing the second boundary.
@@ -68,13 +46,55 @@
 
         # check authorization with new password
         self.non_admin_token_client.auth(self.username,
-                                         new_pass,
+                                         password,
                                          self.tenant_name)
 
+        # Reset auth to get a new token with the new password
+        self.non_admin_users_client.auth_provider.clear_auth()
+        self.non_admin_users_client.auth_provider.credentials.password = (
+            password)
+
+    def _restore_password(self, user_id, old_pass, new_pass):
+        if CONF.identity_feature_enabled.security_compliance:
+            # First we need to clear the password history
+            unique_count = CONF.identity.user_unique_last_password_count
+            for i in range(unique_count):
+                random_pass = data_utils.rand_password()
+                self._update_password(
+                    user_id, original_password=new_pass, password=random_pass)
+                new_pass = random_pass
+
+        self._update_password(
+            user_id, original_password=new_pass, password=old_pass)
+        # Reset auth again to verify the password restore does work.
+        # Clear auth restores the original credentials and deletes
+        # cached auth data
+        self.non_admin_users_client.auth_provider.clear_auth()
+        # NOTE(lbragstad): Fernet tokens are not subsecond aware and
+        # Keystone should only be precise to the second. Sleep to ensure we
+        # are passing the second boundary before attempting to
+        # authenticate.
+        time.sleep(1)
+        self.non_admin_users_client.auth_provider.set_auth()
+
+    @test.idempotent_id('165859c9-277f-4124-9479-a7d1627b0ca7')
+    def test_user_update_own_password(self):
+        old_pass = self.creds.password
+        old_token = self.non_admin_users_client.token
+        new_pass = data_utils.rand_password()
+        user_id = self.creds.user_id
+
+        # to change password back. important for allow_tenant_isolation = false
+        self.addCleanup(self._restore_password, user_id, old_pass, new_pass)
+
+        # user updates own password
+        self._update_password(
+            user_id, original_password=old_pass, password=new_pass)
+
         # authorize with old token should lead to Unauthorized
         self.assertRaises(exceptions.Unauthorized,
                           self.non_admin_token_client.auth_token,
-                          self.non_admin_users_client.token)
+                          old_token)
 
         # authorize with old password should lead to Unauthorized
         self.assertRaises(exceptions.Unauthorized,
diff --git a/tempest/api/identity/v3/test_users.py b/tempest/api/identity/v3/test_users.py
index 1a38f3a..f5b357c 100644
--- a/tempest/api/identity/v3/test_users.py
+++ b/tempest/api/identity/v3/test_users.py
@@ -15,12 +15,18 @@
 
 import time
 
+import testtools
+
 from tempest.api.identity import base
+from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import exceptions
 from tempest import test
 
 
+CONF = config.CONF
+
+
 class IdentityV3UsersTest(base.BaseIdentityV3Test):
 
     @classmethod
@@ -31,36 +37,11 @@
         cls.username = cls.creds.username
         cls.password = cls.creds.password
 
-    @test.idempotent_id('ad71bd23-12ad-426b-bb8b-195d2b635f27')
-    def test_user_update_own_password(self):
-
-        def _restore_password(client, user_id, old_pass, new_pass):
-            # Reset auth to get a new token with the new password
-            client.auth_provider.clear_auth()
-            client.auth_provider.credentials.password = new_pass
-            client.update_user_password(user_id, password=old_pass,
-                                        original_password=new_pass)
-            # Reset auth again to verify the password restore does work.
-            # Clear auth restores the original credentials and deletes
-            # cached auth data
-            client.auth_provider.clear_auth()
-            # NOTE(lbragstad): Fernet tokens are not subsecond aware and
-            # Keystone should only be precise to the second. Sleep to ensure we
-            # are passing the second boundary before attempting to
-            # authenticate.
-            time.sleep(1)
-            client.auth_provider.set_auth()
-
-        old_pass = self.creds.password
-        new_pass = data_utils.rand_password()
-        user_id = self.creds.user_id
-        # to change password back. important for allow_tenant_isolation = false
-        self.addCleanup(_restore_password, self.non_admin_users_client,
-                        user_id, old_pass=old_pass, new_pass=new_pass)
-
-        # user updates own password
+    def _update_password(self, original_password, password):
         self.non_admin_users_client.update_user_password(
-            user_id, password=new_pass, original_password=old_pass)
+            self.user_id,
+            password=password,
+            original_password=original_password)
 
         # NOTE(morganfainberg): Fernet tokens are not subsecond aware and
         # Keystone should only be precise to the second. Sleep to ensure
@@ -68,15 +49,112 @@
         time.sleep(1)
 
         # check authorization with new password
-        self.non_admin_token.auth(user_id=self.user_id, password=new_pass)
+        self.non_admin_token.auth(user_id=self.user_id, password=password)
+
+        # Reset auth to get a new token with the new password
+        self.non_admin_users_client.auth_provider.clear_auth()
+        self.non_admin_users_client.auth_provider.credentials.password = (
+            password)
+
+    def _restore_password(self, old_pass, new_pass):
+        if CONF.identity_feature_enabled.security_compliance:
+            # First we need to clear the password history
+            unique_count = CONF.identity.user_unique_last_password_count
+            for i in range(unique_count):
+                random_pass = data_utils.rand_password()
+                self._update_password(
+                    original_password=new_pass, password=random_pass)
+                new_pass = random_pass
+
+        self._update_password(original_password=new_pass, password=old_pass)
+        # Reset auth again to verify the password restore does work.
+        # Clear auth restores the original credentials and deletes
+        # cached auth data
+        self.non_admin_users_client.auth_provider.clear_auth()
+        # NOTE(lbragstad): Fernet tokens are not subsecond aware and
+        # Keystone should only be precise to the second. Sleep to ensure we
+        # are passing the second boundary before attempting to
+        # authenticate.
+        time.sleep(1)
+        self.non_admin_users_client.auth_provider.set_auth()
+
+    @test.idempotent_id('ad71bd23-12ad-426b-bb8b-195d2b635f27')
+    def test_user_update_own_password(self):
+        old_pass = self.creds.password
+        old_token = self.non_admin_client.token
+        new_pass = data_utils.rand_password()
+
+        # to change password back. important for allow_tenant_isolation = false
+        self.addCleanup(self._restore_password, old_pass, new_pass)
+
+        # user updates own password
+        self._update_password(original_password=old_pass, password=new_pass)
 
         # authorize with old token should lead to IdentityError (404 code)
         self.assertRaises(exceptions.IdentityError,
                           self.non_admin_token.auth,
-                          token=self.non_admin_client.token)
+                          token=old_token)
 
         # authorize with old password should lead to Unauthorized
         self.assertRaises(exceptions.Unauthorized,
                           self.non_admin_token.auth,
                           user_id=self.user_id,
                           password=old_pass)
+
+    @testtools.skipUnless(CONF.identity_feature_enabled.security_compliance,
+                          'Security compliance not available.')
+    @test.idempotent_id('941784ee-5342-4571-959b-b80dd2cea516')
+    def test_password_history_check_self_service_api(self):
+        old_pass = self.creds.password
+        new_pass1 = data_utils.rand_password()
+        new_pass2 = data_utils.rand_password()
+
+        self.addCleanup(self._restore_password, old_pass, new_pass2)
+
+        # Update password
+        self._update_password(original_password=old_pass, password=new_pass1)
+
+        if CONF.identity.user_unique_last_password_count > 1:
+            # Can not reuse a previously set password
+            self.assertRaises(exceptions.BadRequest,
+                              self.non_admin_users_client.update_user_password,
+                              self.user_id,
+                              password=new_pass1,
+                              original_password=new_pass1)
+
+            self.assertRaises(exceptions.BadRequest,
+                              self.non_admin_users_client.update_user_password,
+                              self.user_id,
+                              password=old_pass,
+                              original_password=new_pass1)
+
+        # A different password can be set
+        self._update_password(original_password=new_pass1, password=new_pass2)
+
+    @testtools.skipUnless(CONF.identity_feature_enabled.security_compliance,
+                          'Security compliance not available.')
+    @test.idempotent_id('a7ad8bbf-2cff-4520-8c1d-96332e151658')
+    def test_user_account_lockout(self):
+        password = self.creds.password
+
+        # First, we login using the correct credentials
+        self.non_admin_token.auth(user_id=self.user_id, password=password)
+
+        # Lock user account by using the wrong password to login
+        bad_password = data_utils.rand_password()
+        for i in range(CONF.identity.user_lockout_failure_attempts):
+            self.assertRaises(exceptions.Unauthorized,
+                              self.non_admin_token.auth,
+                              user_id=self.user_id,
+                              password=bad_password)
+
+        # The user account must be locked, so now it is not possible to login
+        # even using the correct password
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_token.auth,
+                          user_id=self.user_id,
+                          password=password)
+
+        # If we wait the required time, the user account will be unlocked
+        time.sleep(CONF.identity.user_lockout_duration + 1)
+        self.token.auth(user_id=self.user_id, password=password)
diff --git a/tempest/config.py b/tempest/config.py
index 9e03b7f..281e283 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -171,7 +171,20 @@
     cfg.BoolOpt('admin_domain_scope',
                 default=False,
                 help="Whether keystone identity v3 policy required "
-                     "a domain scoped token to use admin APIs")
+                     "a domain scoped token to use admin APIs"),
+    # Security Compliance (PCI-DSS)
+    cfg.IntOpt('user_lockout_failure_attempts',
+               default=2,
+               help="The number of unsuccessful login attempts the user is "
+                    "allowed before having the account locked."),
+    cfg.IntOpt('user_lockout_duration',
+               default=5,
+               help="The number of seconds a user account will remain "
+                    "locked."),
+    cfg.IntOpt('user_unique_last_password_count',
+               default=2,
+               help="The number of passwords for a user that must be unique "
+                    "before an old password can be reused."),
 ]
 
 service_clients_group = cfg.OptGroup(name='service-clients',
@@ -208,7 +221,11 @@
     # of life.
     cfg.BoolOpt('reseller',
                 default=False,
-                help='Does the environment support reseller?')
+                help='Does the environment support reseller?'),
+    cfg.BoolOpt('security_compliance',
+                default=False,
+                help='Does the environment have the security compliance '
+                     'settings enabled?')
 ]
 
 compute_group = cfg.OptGroup(name='compute',