Repository: freedomofpress/securedrop

Issue:
Temporarily disable safety check
## Description

We'll need to temporarily disable safety in order to merge until #2926 is resolved (and we'll need to cherry pick the disabling of safety into the 0.5.2 release branch). 

## User Stories

As a SecureDrop maintainer, I don't want to merge with failing CI. 
CI test failures obscure debugging output
# Feature request

## Description

The CI run obscures results from the various test suites, making certain failures difficult to understand. See for example [Circle CI build 5914](https://circleci.com/gh/freedomofpress/securedrop/5914), which shows:


> TASK [fail] ******************************************************************** Friday 05 January 2018 19:42:25 +0000 (0:03:20.042) 0:03:24.094 ******** fatal: [app-staging]: FAILED! => {"changed": false, "failed": true, "msg": "Failed as requested from task"}


The relevant task in `molecule/aws/side_effect.yml` is:

```
    - fail:
      when: app_test_register|failed or testinfra_results|failed
```

It's not immediately clear from the error output _which_ specific test failed, or even which test suite contained the error. We should break that single untitled task into two named tasks, with separate conditionals. Consider using `assert` rather than `fail` because then we can supply a custom `fail_msg` with informative output for developers.


## User Stories

As a developer, if tests fail I want to know exactly what failed so I can fix it. I don't want to cajole answers out of a stubborn and inscrutable oracle _before_ asking team members for review.



Discussion:

I intially tried to fix this in PR #2796 by adding an annotation to the junit file . If that works correctly.. it will show a notification at the top on the Circle UI. Unfortunately... its not picking up for whatever reason... GRRR .. so i can circle back to this but for now `3591fc7` doesnt fix the issue as described but it did add more artifacts for dev to search raw test execution output.

Resolving patch:
diff --git a/install_files/ansible-base/roles/upgrade/files/0.3pre_upgrade.py b/install_files/ansible-base/roles/upgrade/files/0.3pre_upgrade.py
deleted file mode 100755
--- a/install_files/ansible-base/roles/upgrade/files/0.3pre_upgrade.py
+++ /dev/null
@@ -1,197 +0,0 @@
-#!/usr/bin/python2.7
-
-from datetime import datetime
-import os
-import shutil
-import sqlite3
-import subprocess
-import sys
-import tarfile
-import traceback
-
-
-def backup_app():
-    tar_fn = 'backup-app-{}.tar.bz2'.format(datetime.now().strftime("%Y-%m-%d--%H-%M-%S"))
-    with tarfile.open(tar_fn, 'w:bz2') as t:
-        t.add('/var/lib/securedrop/')
-        t.add('/var/lib/tor/services/')
-        t.add('/var/www/securedrop/config.py')
-        try:
-            t.add('/var/www/securedrop/static/i/logo.png')
-        except OSError:
-            print "[!] Expected but non-essential file ('logo.png') not found. Continuing..."
-    print "**  Backed up system to {} before migrating.".format(tar_fn)
-
-
-def backup_mon():
-    # The only thing we have to back up for the monitor server is the SSH ATHS cert.
-    # All other required values are available in prod-specific.yml from the installation.
-    tar_fn = 'backup-mon-{}.tar.bz2'.format(datetime.now().strftime("%Y-%m-%d--%H-%M-%S"))
-    with tarfile.open(tar_fn, 'w:bz2') as t:
-        t.add('/var/lib/tor/services/')
-    print "**  Backed up system to {} before migrating.".format(tar_fn)
-
-
-def secure_unlink(path):
-    subprocess.check_call(['srm', '-r', path])
-
-
-def cleanup_deleted_sources(store_dir, c):
-    """
-    In 0.3pre and 0.3, there were two bugs that could potentially lead
-    to the source directory failing to be deleted when a source was
-    deleted from the Journalist Interface. We clean up these leftover
-    directories as part of the migration.
-
-    These sources can be identified because they have a source_dir in
-    the store_dir, but no corresponding Source entry in the database.
-
-    See https://github.com/freedomofpress/securedrop/pull/944 for context.
-    """
-    for source_dir in os.listdir(store_dir):
-        try:
-            source = c.execute("SELECT * FROM sources WHERE filesystem_id=?",
-                (source_dir,)).fetchone()
-            if not source:
-                print "Deleting source with no db entry ('{}')...".format(source_dir)
-                secure_unlink(os.path.join(store_dir, source_dir))
-        except Exception as e:
-            print "\n!!  Error occurred cleaning up deleted sources for source {}".format(source_dir)
-            print "Source had {} submissions".format(len(os.listdir(os.path.join(store_dir, source_dir))))
-            print traceback.format_exc()
-
-
-def get_db_connection():
-    db_path = "/var/lib/securedrop/db.sqlite"
-    assert os.path.isfile(db_path)
-    conn = sqlite3.connect(db_path)
-    return conn, conn.cursor()
-
-
-def migrate_app_db():
-    store_dir = "/var/lib/securedrop/store"
-    conn, c = get_db_connection()
-
-    # Before modifying the database, clean up any source directories that were
-    # left on the filesystem after the sources were deleted.
-    cleanup_deleted_sources(store_dir, c)
-
-    # To get CREATE TABLE from SQLAlchemy:
-    # >>> import db
-    # >>> from sqlalchemy.schema import CreateTable
-    # >>> print CreateTable(db.Journalist.__table__).compile(db.engine)
-    # Or, add `echo=True` to the engine constructor.
-    # CREATE TABLE replies
-    c.execute("""
-CREATE TABLE replies (
-	id INTEGER NOT NULL,
-	journalist_id INTEGER,
-	source_id INTEGER,
-	filename VARCHAR(255) NOT NULL,
-	size INTEGER NOT NULL,
-	PRIMARY KEY (id),
-	FOREIGN KEY(journalist_id) REFERENCES journalists (id),
-	FOREIGN KEY(source_id) REFERENCES sources (id)
-)""")
-
-    # Fill in replies from the replies in STORE_DIR at the time of the migration
-    #
-    # Caveats:
-    #
-    # 1. Before we added the `replies` table, we did not keep track of which
-    # journalist wrote the reply. There is no way for us to reverse-engineer
-    # that information, so the migration will default to saying they were all
-    # created by the first journalist (arbitrarily). Since we do not surface
-    # this in the UI yet anyway, it should not be a big deal.
-    #
-    # 2. We do not try to get the order of the (autoincrementing primary key)
-    # reply_id to match the order in which the replies were created (which could
-    # be inferred from the file timestamps, since we only normalize submission
-    # timestamps and not reply timestamps) since this order is not used anywhere
-    # in the code.
-
-    # Copy from db.py to compute filesystem-safe journalist filenames
-    def journalist_filename(s):
-        valid_chars = 'abcdefghijklmnopqrstuvwxyz1234567890-_'
-        return ''.join([c for c in s.lower().replace(' ', '_') if c in valid_chars])
-
-    reply_id = 1
-    for source_dir in os.listdir(store_dir):
-        try:
-            source_id, journalist_designation = c.execute(
-                "SELECT id, journalist_designation FROM sources WHERE filesystem_id=?",
-                (source_dir,)).fetchone()
-        except sqlite3.Error as e:
-            print "!!\tError occurred migrating replies for source {}".format(source_dir)
-            print traceback.format_exc()
-            continue
-
-        for filename in os.listdir(os.path.join(store_dir, source_dir)):
-            if "-reply.gpg" not in filename:
-                continue
-
-            # Rename the reply file from 0.3pre convention to 0.3 convention
-            interaction_count = filename.split('-')[0]
-            new_filename = "{}-{}-reply.gpg".format(interaction_count,
-                journalist_filename(journalist_designation))
-            os.rename(os.path.join(store_dir, source_dir, filename),
-                      os.path.join(store_dir, source_dir, new_filename))
-
-            # need id, journalist_id, source_id, filename, size
-            journalist_id = 1 # *shrug*
-            full_path = os.path.join(store_dir, source_dir, new_filename)
-            size = os.stat(full_path).st_size
-            c.execute("INSERT INTO replies VALUES (?,?,?,?,?)",
-                      (reply_id, journalist_id, source_id, new_filename, size))
-            reply_id += 1 # autoincrement for next reply
-
-    # CREATE TABLE journalist_login_attempts
-    c.execute("""
-CREATE TABLE journalist_login_attempt (
-	id INTEGER NOT NULL,
-	timestamp DATETIME,
-	journalist_id INTEGER,
-	PRIMARY KEY (id),
-	FOREIGN KEY(journalist_id) REFERENCES journalists (id)
-)""")
-
-    # ALTER TABLE journalists, add last_token column
-    c.execute("""ALTER TABLE journalists ADD COLUMN last_token VARCHAR(6)""")
-
-    # Save changes and close connection
-    conn.commit()
-    conn.close()
-
-
-def app_db_migrated():
-    """To make the upgrade role idempotent, we need to skip migrating the
-    database if it has already been modified. The best way to do this
-    is to check whether the last sql command in `migrate_app_db`
-    (ALTER TABLE to add the last_token column to the journalists
-    table) succeeded. If so, we can assume the database app migration
-    succeeded and can safely skip doing it again.
-
-    """
-    conn, c = get_db_connection()
-    journalist_tables = c.execute('PRAGMA table_info(journalists)').fetchall()
-    table_names = set([table[1] for table in journalist_tables])
-    return 'last_token' in table_names
-
-
-def main():
-    if len(sys.argv) <= 1:
-        print "Usage: 0.3pre_upgrade.py app|mon"
-        sys.exit(1)
-
-    server_role = sys.argv[1]
-    assert server_role in ("app", "mon")
-
-    if server_role == "app":
-        backup_app()
-        if not app_db_migrated():
-            migrate_app_db()
-    else:
-        backup_mon()
-
-if __name__ == "__main__":
-    main()
diff --git a/securedrop/crypto_util.py b/securedrop/crypto_util.py
--- a/securedrop/crypto_util.py
+++ b/securedrop/crypto_util.py
@@ -5,7 +5,7 @@
 import os
 import subprocess
 
-from Crypto.Random import random
+from Cryptodome.Random import random
 import gnupg
 from gnupg._util import _is_stream, _make_binary_stream
 import scrypt
diff --git a/securedrop/secure_tempfile.py b/securedrop/secure_tempfile.py
--- a/securedrop/secure_tempfile.py
+++ b/securedrop/secure_tempfile.py
@@ -4,9 +4,9 @@
 from tempfile import _TemporaryFileWrapper
 
 from gnupg._util import _STREAMLIKE_TYPES
-from Crypto.Cipher import AES
-from Crypto.Random import random
-from Crypto.Util import Counter
+from Cryptodome.Cipher import AES
+from Cryptodome.Random import random
+from Cryptodome.Util import Counter
 
 
 class SecureTemporaryFile(_TemporaryFileWrapper, object):


Test patch:
diff --git a/docs/development/testing_continuous_integration.rst b/docs/development/testing_continuous_integration.rst
--- a/docs/development/testing_continuous_integration.rst
+++ b/docs/development/testing_continuous_integration.rst
@@ -3,23 +3,10 @@
 Testing: CI
 ===========
 
-The SecureDrop project uses multiple automated third-party solutions
-for running automated test suites on code changes:
+The SecureDrop project uses CircleCI_ for running automated test suites on code changes:
 
-  * Travis_
-  * CircleCI_
-
-.. _Travis: https://travis-ci.org/freedomofpress/securedrop/
 .. _CircleCI: http://circleci.com/gh/freedomofpress/securedrop/
 
-Travis tests
-------------
-
-The Travis_ test suite provisions the development VM and runs the application
-test suite against the latest version of the code. It also performs basic
-linting and validation, e.g. checking for mistakes in the Sphinx documentation
-(see :doc:`documentation_guidelines`).
-
 CI test layout
 --------------
 
@@ -29,10 +16,9 @@ The relevant files for configuring the CI tests are: ::
     ├── devops
     │   ├── inventory <-- environment specific inventory
     │   ├── playbooks <-- playbooks to start CI boxes
-    │   ├── scripts   <-- shell wrapper scripts 
+    │   ├── scripts   <-- shell wrapper scripts
     │   ├── templates <-- contains templates for ansible tasks
     │   └── vars <-- environment specific variables
-    ├── .travis.yml  <--- config for development tests on travis
     └── Makefile  <-- defines make task shortcuts
 
 The files under ``devops/`` are used to create a minimized staging environment
@@ -46,6 +32,9 @@ The staging environment tests will run automatically in CircleCI,
 when changes are submitted by Freedom of the Press Foundation staff
 (i.e. members of the ``freedomofpress`` GitHub organization).
 
+It also performs basic linting and validation, e.g. checking for mistakes in
+the Sphinx documentation.
+
 .. tip:: You will need an Amazon Web Services EC2 account to proceed.
          See the `AWS Getting Started Guide`_ for detailed instructions.
 
diff --git a/install_files/ansible-base/roles/ossec-server/files/test_admin_key.pub b/install_files/ansible-base/roles/ossec/files/test_admin_key.pub
similarity index 100%
rename from install_files/ansible-base/roles/ossec-server/files/test_admin_key.pub
rename to install_files/ansible-base/roles/ossec/files/test_admin_key.pub
diff --git a/install_files/ansible-base/roles/ossec-server/files/test_admin_key.sec b/install_files/ansible-base/roles/ossec/files/test_admin_key.sec
similarity index 100%
rename from install_files/ansible-base/roles/ossec-server/files/test_admin_key.sec
rename to install_files/ansible-base/roles/ossec/files/test_admin_key.sec
diff --git a/molecule/ansible-config/tests/test_max_fail_percentage.py b/molecule/ansible-config/tests/test_max_fail_percentage.py
new file mode 100644
--- /dev/null
+++ b/molecule/ansible-config/tests/test_max_fail_percentage.py
@@ -0,0 +1,80 @@
+import os
+
+import pytest
+import yaml
+
+
+# Lots of parent directories to dig out of the Molecule test dir.
+# Could also inspect the Molecule env vars and go from there.
+REPO_ROOT = os.path.abspath(os.path.join(__file__,
+                                         os.path.pardir,
+                                         os.path.pardir,
+                                         os.path.pardir,
+                                         os.path.pardir,
+                                         ))
+ANSIBLE_BASE = os.path.join(REPO_ROOT, 'install_files', 'ansible-base')
+
+
+def find_ansible_playbooks():
+    """
+    Test helper to generate list of filepaths for SecureDrop
+    Ansible playbooks. All files will be validated to contain the
+    max_fail option.
+    """
+    playbooks = []
+    # Not using os.walk since all SecureDrop playbooks are in top-level
+    # of the "ansible-base" directory, and *many* YAML files that are
+    # not playbooks reside in subdirectories.
+    for f in os.listdir(ANSIBLE_BASE):
+        # Assume all YAML files in directory are playbooks.
+        if f.endswith(".yml"):
+            # Ignore deprecated production vars file.
+            if f != "prod-specific.yml":
+                playbooks.append(os.path.join(ANSIBLE_BASE, f))
+    # Sanity checking to make sure list of playbooks is not empty.
+    assert len(playbooks) > 0
+    return playbooks
+
+
+@pytest.mark.parametrize('playbook', find_ansible_playbooks())
+def test_max_fail_percentage(host, playbook):
+    """
+    All SecureDrop playbooks should set `max_fail_percentage` to "0"
+    on each and every play. Doing so ensures that an error on a single
+    host constitutes a play failure.
+
+    In conjunction with the `any_errors_fatal` option, tested separately,
+    this will achieve a "fail fast" behavior from Ansible.
+
+    There's no ansible.cfg option to set for max_fail_percentage, which would
+    allow for a single DRY update that would apply automatically to all
+    invocations of `ansible-playbook`. Therefore this test, which will
+    search for the line present in all playbooks.
+
+    Technically it's only necessary that plays targeting multiple hosts use
+    the parameter, but we'll play it safe and require it everywhere,
+    to avoid mistakes down the road.
+    """
+    with open(playbook, 'r') as f:
+        playbook_yaml = yaml.safe_load(f)
+        # Descend into playbook list structure to validate play attributes.
+        for play in playbook_yaml:
+            assert 'max_fail_percentage' in play
+            assert play['max_fail_percentage'] == 0
+
+
+@pytest.mark.parametrize('playbook', find_ansible_playbooks())
+def test_any_errors_fatal(host, playbook):
+    """
+    All SecureDrop playbooks should set `any_errors_fatal` to "yes"
+    on each and every play. In conjunction with `max_fail_percentage` set
+    to "0", doing so ensures that any errors will cause an immediate failure
+    on the playbook.
+    """
+    with open(playbook, 'r') as f:
+        playbook_yaml = yaml.safe_load(f)
+        # Descend into playbook list structure to validate play attributes.
+        for play in playbook_yaml:
+            assert 'any_errors_fatal' in play
+            # Ansible coerces booleans, so bare assert is sufficient
+            assert play['any_errors_fatal']
diff --git a/securedrop/tests/functional/functional_test.py b/securedrop/tests/functional/functional_test.py
--- a/securedrop/tests/functional/functional_test.py
+++ b/securedrop/tests/functional/functional_test.py
@@ -12,7 +12,7 @@
 import traceback
 import requests
 
-from Crypto import Random
+from Cryptodome import Random
 from selenium import webdriver
 from selenium.common.exceptions import (WebDriverException,
                                         NoAlertPresentException)
diff --git a/testinfra/mon/test_ossec.py b/testinfra/mon/test_ossec.py
--- a/testinfra/mon/test_ossec.py
+++ b/testinfra/mon/test_ossec.py
@@ -20,7 +20,6 @@ def test_ossec_package(Package, package):
     assert Package(package).is_installed
 
 
-@pytest.mark.xfail(strict=True)
 def test_ossec_connectivity(Command, Sudo):
     """
     Ensure ossec-server machine has active connection to the ossec-agent.