Exemplo n.º 1
0
def add_reply(source: Source, journalist: Journalist,
              journalist_who_saw: Optional[Journalist]) -> None:
    """
    Adds a single reply to a source.
    """
    record_source_interaction(source)
    fname = "{}-{}-reply.gpg".format(source.interaction_count,
                                     source.journalist_filename)
    EncryptionManager.get_default().encrypt_journalist_reply(
        for_source_with_filesystem_id=source.filesystem_id,
        reply_in=next(replies),
        encrypted_reply_path_out=Path(Storage.get_default().path(
            source.filesystem_id, fname)),
    )
    reply = Reply(journalist, source, fname, Storage.get_default())
    db.session.add(reply)

    # Journalist who replied has seen the reply
    author_seen_reply = SeenReply(reply=reply, journalist=journalist)
    db.session.add(author_seen_reply)

    if journalist_who_saw:
        other_seen_reply = SeenReply(reply=reply,
                                     journalist=journalist_who_saw)
        db.session.add(other_seen_reply)

    db.session.commit()
def upgrade() -> None:
    with op.batch_alter_table("replies", schema=None) as batch_op:
        batch_op.add_column(
            sa.Column("checksum", sa.String(length=255), nullable=True))

    with op.batch_alter_table("submissions", schema=None) as batch_op:
        batch_op.add_column(
            sa.Column("checksum", sa.String(length=255), nullable=True))

    op.create_table(
        "revoked_tokens",
        sa.Column("id", sa.Integer(), nullable=False),
        sa.Column("journalist_id", sa.Integer(), nullable=True),
        sa.Column("token", sa.Text(), nullable=False),
        sa.ForeignKeyConstraint(["journalist_id"], ["journalists.id"]),
        sa.PrimaryKeyConstraint("id"),
        sa.UniqueConstraint("token"),
    )

    try:
        app = create_app(config)

        # we need an app context for the rq worker extension to work properly
        with app.app_context():
            conn = op.get_bind()
            query = sa.text(
                """SELECT submissions.id, sources.filesystem_id, submissions.filename
                               FROM submissions
                               INNER JOIN sources
                               ON submissions.source_id = sources.id
                            """)
            for (sub_id, filesystem_id, filename) in conn.execute(query):
                full_path = Storage.get_default().path(filesystem_id, filename)
                create_queue().enqueue(
                    queued_add_checksum_for_file,
                    Submission,
                    int(sub_id),
                    full_path,
                    app.config["SQLALCHEMY_DATABASE_URI"],
                )

            query = sa.text(
                """SELECT replies.id, sources.filesystem_id, replies.filename
                               FROM replies
                               INNER JOIN sources
                               ON replies.source_id = sources.id
                            """)
            for (rep_id, filesystem_id, filename) in conn.execute(query):
                full_path = Storage.get_default().path(filesystem_id, filename)
                create_queue().enqueue(
                    queued_add_checksum_for_file,
                    Reply,
                    int(rep_id),
                    full_path,
                    app.config["SQLALCHEMY_DATABASE_URI"],
                )
    except:  # noqa
        if raise_errors:
            raise
Exemplo n.º 3
0
def _create_source_and_submission(config_in_use: SecureDropConfig) -> Path:
    """Directly create a source and a submission within the app.

    Some tests for the journalist app require a submission to already be present, and this
    function is used to create the source user and submission when the journalist app starts.

    This implementation is much faster than using Selenium to navigate the source app in order
    to create a submission: it takes 0.2s to run, while the Selenium implementation takes 7s.
    """
    # This function will be called in a separate Process that runs the app
    # Hence the late imports
    from encryption import EncryptionManager
    from models import Submission
    from passphrases import PassphraseGenerator
    from source_user import create_source_user
    from store import Storage, add_checksum_for_file
    from tests.functional.db_session import get_database_session

    # Create a source
    passphrase = PassphraseGenerator.get_default().generate_passphrase()
    with get_database_session(
            database_uri=config_in_use.DATABASE_URI) as db_session:
        source_user = create_source_user(
            db_session=db_session,
            source_passphrase=passphrase,
            source_app_storage=Storage.get_default(),
        )
        source_db_record = source_user.get_db_record()
        EncryptionManager.get_default().generate_source_key_pair(source_user)

        # Create a file submission from this source
        source_db_record.interaction_count += 1
        app_storage = Storage.get_default()
        encrypted_file_name = app_storage.save_file_submission(
            filesystem_id=source_user.filesystem_id,
            count=source_db_record.interaction_count,
            journalist_filename=source_db_record.journalist_filename,
            filename="filename.txt",
            stream=BytesIO(b"File with S3cr3t content"),
        )
        submission = Submission(source_db_record, encrypted_file_name,
                                app_storage)
        db_session.add(submission)
        source_db_record.pending = False
        source_db_record.last_updated = datetime.now(timezone.utc)
        db_session.commit()

        submission_file_path = app_storage.path(source_user.filesystem_id,
                                                submission.filename)
        add_checksum_for_file(
            session=db_session,
            db_obj=submission,
            file_path=submission_file_path,
        )

        return Path(submission_file_path)
Exemplo n.º 4
0
def delete_file_object(file_object: Union[Submission, Reply]) -> None:
    path = Storage.get_default().path(file_object.source.filesystem_id,
                                      file_object.filename)
    try:
        Storage.get_default().move_to_shredder(path)
    except ValueError as e:
        current_app.logger.error("could not queue file for deletion: %s", e)
        raise
    finally:
        db.session.delete(file_object)
        db.session.commit()
Exemplo n.º 5
0
class Pipeline():
    def __init__(self):
        self.session = requests.Session()
        self.session.headers = {'user-agent': 'shr-podcasts-bot'}
        self.scraper = Scraper(self.session)
        self.parser = Parser(self.session)
        self.storage = Storage()

    def run(self, root, start_page):
        podcasts = (self.parser.parse_feed(feed)
                    for feed in self.scraper.scrape(root, start_page))
        for podcast in filter(None, podcasts):
            self.storage.store_podcast(podcast)
Exemplo n.º 6
0
class Pipeline():
    def __init__(self):
        self.session = requests.Session()
        self.session.headers = {'user-agent': 'shr-podcasts-bot'}
        self.scraper = Scraper(self.session)
        self.parser = Parser(self.session)
        self.storage = Storage()

    def run(self, root, start_page):
        podcasts = (self.parser.parse_feed(feed) for feed in
                    self.scraper.scrape(root, start_page))
        for podcast in filter(None, podcasts):
            self.storage.store_podcast(podcast)
Exemplo n.º 7
0
def delete_collection(filesystem_id: str) -> None:
    """deletes source account including files and reply key"""
    # Delete the source's collection of submissions
    path = Storage.get_default().path(filesystem_id)
    if os.path.exists(path):
        Storage.get_default().move_to_shredder(path)

    # Delete the source's reply keypair
    EncryptionManager.get_default().delete_source_key_pair(filesystem_id)

    # Delete their entry in the db
    source = get_source(filesystem_id, include_deleted=True)
    db.session.delete(source)
    db.session.commit()
def allocate_application(arguments):
    application = Storage(
        bootstrap=arguments.bootstrap,
        miner=arguments.miner,
    )

    return application
Exemplo n.º 9
0
 def __init__(self, source: Source, filename: str,
              storage: Storage) -> None:
     self.source_id = source.id
     self.filename = filename
     self.uuid = str(uuid.uuid4())
     self.size = os.stat(storage.path(source.filesystem_id,
                                      filename)).st_size
Exemplo n.º 10
0
class MainHTTPHandler(BaseHTTPRequestHandler):
    router = {
        "method": method_handler
    }
    store = Storage()

    def get_request_id(self, headers):
        return headers.get('HTTP_X_REQUEST_ID', uuid.uuid4().hex)

    def process_request_(self, request, response, data_string, context):
        """Process user's request, return response and code of response"""

        path = self.path.strip("/")
        logging.info("%s: %s %s" % (self.path, data_string, context["request_id"]))

        if path in self.router:
            try:
                response, code = self.router[path]({"body": request, "headers": self.headers},
                                                   context, self.store)
            except (UserRequestError, StorageError) as e:
                if isinstance(e, StorageIsDeadError):
                    response, code = e.args[0], INTERNAL_ERROR
                else:
                    response, code = e.args[0], INVALID_REQUEST
            except Exception as e:
                logging.exception("Unexpected error: %s" % e)
                code = INTERNAL_ERROR
        else:
            code = NOT_FOUND

        return response, code

    def do_POST(self):
        """Only posts requests allowed"""

        response, code = {}, OK
        context = {"request_id": self.get_request_id(self.headers)}
        request = None
        try:
            data_string = self.rfile.read(int(self.headers['Content-Length']))
            request = json.loads(data_string.decode("utf-8"))
        except Exception as e:
            code = BAD_REQUEST

        if request:
            response, code = self.process_request_(request, response,
                                                   data_string, context)

        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        if code not in ERRORS:
            r = {"response": response, "code": code}
        else:
            r = {"error": response or ERRORS.get(code, "Unknown Error"), "code": code}
        context.update(r)
        logging.info(context)
        self.wfile.write(json.dumps(r).encode("utf-8"))
        return
Exemplo n.º 11
0
    def __init__(self, config):
        Helper.__init__(self)
        self.config = config
        self.app = create_app(config)

        # as this class requires access to the Storage object, which is no longer
        # attached to app, we create it here and mock the call to return it below.
        self.storage = Storage(config.STORE_DIR, config.TEMP_DIR)
Exemplo n.º 12
0
def submit_file(source: Source,
                journalist_who_saw: Optional[Journalist]) -> None:
    """
    Adds a single file submitted by a source.
    """
    record_source_interaction(source)
    fpath = Storage.get_default().save_file_submission(
        source.filesystem_id,
        source.interaction_count,
        source.journalist_filename,
        "memo.txt",
        io.BytesIO(b"This is an example of a plain text file upload."),
    )
    submission = Submission(source, fpath, Storage.get_default())
    db.session.add(submission)

    if journalist_who_saw:
        seen_file = SeenFile(file=submission, journalist=journalist_who_saw)
        db.session.add(seen_file)
Exemplo n.º 13
0
def submit_message(source: Source,
                   journalist_who_saw: Optional[Journalist]) -> None:
    """
    Adds a single message submitted by a source.
    """
    record_source_interaction(source)
    fpath = Storage.get_default().save_message_submission(
        source.filesystem_id,
        source.interaction_count,
        source.journalist_filename,
        next(messages),
    )
    submission = Submission(source, fpath, Storage.get_default())
    db.session.add(submission)

    if journalist_who_saw:
        seen_message = SeenMessage(message=submission,
                                   journalist=journalist_who_saw)
        db.session.add(seen_message)
Exemplo n.º 14
0
def test_storage() -> Generator[Storage, None, None]:
    # Setup the filesystem for the storage object
    with TemporaryDirectory() as data_dir_name:
        data_dir = Path(data_dir_name)
        store_dir = data_dir / "store"
        store_dir.mkdir()
        tmp_dir = data_dir / "tmp"
        tmp_dir.mkdir()

        storage = Storage(str(store_dir), str(tmp_dir))

        yield storage
Exemplo n.º 15
0
    def lookup(logged_in_source: SourceUser) -> str:
        replies = []
        logged_in_source_in_db = logged_in_source.get_db_record()
        source_inbox = Reply.query.filter_by(
            source_id=logged_in_source_in_db.id, deleted_by_source=False
        ).all()

        first_submission = logged_in_source_in_db.interaction_count == 0

        if first_submission:
            min_message_length = InstanceConfig.get_default().initial_message_min_len
        else:
            min_message_length = 0

        for reply in source_inbox:
            reply_path = Storage.get_default().path(
                logged_in_source.filesystem_id,
                reply.filename,
            )
            try:
                with io.open(reply_path, "rb") as f:
                    contents = f.read()
                decrypted_reply = EncryptionManager.get_default().decrypt_journalist_reply(
                    for_source_user=logged_in_source, ciphertext_in=contents
                )
                reply.decrypted = decrypted_reply
            except UnicodeDecodeError:
                current_app.logger.error("Could not decode reply %s" % reply.filename)
            except FileNotFoundError:
                current_app.logger.error("Reply file missing: %s" % reply.filename)
            else:
                reply.date = datetime.utcfromtimestamp(os.stat(reply_path).st_mtime)
                replies.append(reply)

        # Sort the replies by date
        replies.sort(key=operator.attrgetter("date"), reverse=True)

        # If not done yet, generate a keypair to encrypt replies from the journalist
        encryption_mgr = EncryptionManager.get_default()
        try:
            encryption_mgr.get_source_public_key(logged_in_source.filesystem_id)
        except GpgKeyNotFoundError:
            encryption_mgr.generate_source_key_pair(logged_in_source)

        return render_template(
            "lookup.html",
            is_user_logged_in=True,
            allow_document_uploads=InstanceConfig.get_default().allow_document_uploads,
            replies=replies,
            min_len=min_message_length,
            new_user_codename=session.get("new_user_codename", None),
            form=SubmissionForm(),
        )
Exemplo n.º 16
0
    def download_single_file(filesystem_id: str, fn: str) -> werkzeug.Response:
        """
        Marks the file being download (the file being downloaded is either a submission message,
        submission file attachement, or journalist reply) as seen by the current logged-in user and
        send the file to a client to be saved or opened.
        """
        if ".." in fn or fn.startswith("/"):
            abort(404)

        file = Storage.get_default().path(filesystem_id, fn)
        if not Path(file).is_file():
            flash(
                gettext(
                    "Your download failed because the file could not be found. An admin can find "
                    + "more information in the system and monitoring logs."),
                "error",
            )
            current_app.logger.error("File {} not found".format(file))
            return redirect(url_for("col.col", filesystem_id=filesystem_id))

        # mark as seen by the current user
        try:
            journalist = g.get("user")
            if fn.endswith("reply.gpg"):
                reply = Reply.query.filter(Reply.filename == fn).one()
                mark_seen([reply], journalist)
            elif fn.endswith("-doc.gz.gpg") or fn.endswith("doc.zip.gpg"):
                submitted_file = Submission.query.filter(
                    Submission.filename == fn).one()
                mark_seen([submitted_file], journalist)
            else:
                message = Submission.query.filter(
                    Submission.filename == fn).one()
                mark_seen([message], journalist)
        except NoResultFound as e:
            current_app.logger.error("Could not mark {} as seen: {}".format(
                fn, e))

        return send_file(Storage.get_default().path(filesystem_id, fn),
                         mimetype="application/pgp-encrypted")
Exemplo n.º 17
0
class MainHTTPHandler(BaseHTTPRequestHandler):
    router = {"method": method_handler}
    store = Storage(RedisConnection, STORE_CONFIG)

    def get_request_id(self, headers):
        return headers.get('HTTP_X_REQUEST_ID', uuid.uuid4().hex)

    def do_POST(self):
        response, code = {}, OK
        context = {"request_id": self.get_request_id(self.headers)}
        request = None
        try:
            data_string = self.rfile.read(int(self.headers['Content-Length']))
            print data_string
            request = json.loads(data_string)  # in Unicode
        except:
            code = BAD_REQUEST

        if request:
            path = self.path.strip("/")
            logging.info("%s: %s %s" %
                         (self.path, data_string, context["request_id"]))
            if path in self.router:
                try:
                    response, code = self.router[path]({
                        "body": request,
                        "headers": self.headers
                    }, context, self.store)
                except Exception, e:
                    logging.exception("Unexpected error: %s" % e)
                    code = INTERNAL_ERROR
            else:
                code = NOT_FOUND

        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        if code not in ERRORS:
            r = {"response": response, "code": code}
        else:
            r = {
                "error": response or ERRORS.get(code, "Unknown Error"),
                "code": code
            }
        context.update(r)
        logging.info(context)

        # save correct unicode in response
        response_data = json.dumps(r, sort_keys=True,
                                   ensure_ascii=False).encode('utf8')
        self.wfile.write(response_data)
        return
Exemplo n.º 18
0
def serve_file_with_etag(db_obj: Union[Reply, Submission]) -> flask.Response:
    file_path = Storage.get_default().path(db_obj.source.filesystem_id,
                                           db_obj.filename)
    response = send_file(file_path,
                         mimetype="application/pgp-encrypted",
                         as_attachment=True,
                         etag=False)  # Disable Flask default ETag

    if not db_obj.checksum:
        add_checksum_for_file(db.session, db_obj, file_path)

    response.direct_passthrough = False
    response.headers["Etag"] = db_obj.checksum
    return response
Exemplo n.º 19
0
def upgrade() -> None:
    conn = op.get_bind()
    submissions = conn.execute(
        sa.text(raw_sql_grab_orphaned_objects("submissions"))).fetchall()

    replies = conn.execute(sa.text(
        raw_sql_grab_orphaned_objects("replies"))).fetchall()

    try:
        app = create_app(config)
        with app.app_context():
            for submission in submissions:
                try:
                    conn.execute(
                        sa.text("""
                        DELETE FROM submissions
                        WHERE id=:id
                    """).bindparams(id=submission.id))

                    path = Storage.get_default().path_without_filesystem_id(
                        submission.filename)
                    Storage.get_default().move_to_shredder(path)
                except NoFileFoundException:
                    # The file must have been deleted by the admin, remove the row
                    conn.execute(
                        sa.text("""
                        DELETE FROM submissions
                        WHERE id=:id
                    """).bindparams(id=submission.id))
                except TooManyFilesException:
                    pass

            for reply in replies:
                try:
                    conn.execute(
                        sa.text("""
                            DELETE FROM replies
                            WHERE id=:id
                        """).bindparams(id=reply.id))

                    path = Storage.get_default().path_without_filesystem_id(
                        reply.filename)
                    Storage.get_default().move_to_shredder(path)
                except NoFileFoundException:
                    # The file must have been deleted by the admin, remove the row
                    conn.execute(
                        sa.text("""
                            DELETE FROM replies
                            WHERE id=:id
                        """).bindparams(id=reply.id))
                except TooManyFilesException:
                    pass
    except:  # noqa
        if raise_errors:
            raise
Exemplo n.º 20
0
    def create() -> werkzeug.Response:
        if SessionManager.is_user_logged_in(db_session=db.session):
            flash_msg(
                "notification",
                None,
                gettext(
                    "You are already logged in. Please verify your codename as it "
                    "may differ from the one displayed on the previous page."
                ),
            )
        else:
            # Ensure the codenames have not expired
            date_codenames_expire = session.get("codenames_expire")
            if not date_codenames_expire or datetime.now(timezone.utc) >= date_codenames_expire:
                return clear_session_and_redirect_to_logged_out_page(flask_session=session)

            tab_id = request.form["tab_id"]
            codename = session["codenames"][tab_id]
            del session["codenames"]

            try:
                current_app.logger.info("Creating new source user...")
                create_source_user(
                    db_session=db.session,
                    source_passphrase=codename,
                    source_app_storage=Storage.get_default(),
                )
            except (SourcePassphraseCollisionError, SourceDesignationCollisionError) as e:
                current_app.logger.error("Could not create a source: {}".format(e))
                flash_msg(
                    "error",
                    None,
                    gettext(
                        "There was a temporary problem creating your account. Please try again."
                    ),
                )
                return redirect(url_for(".index"))

            # All done - source user was successfully created
            current_app.logger.info("New source user created")
            session["new_user_codename"] = codename
            SessionManager.log_user_in(
                db_session=db.session, supplied_passphrase=DicewarePassphrase(codename)
            )

        return redirect(url_for(".lookup"))
Exemplo n.º 21
0
def download(
    zip_basename: str,
    submissions: List[Union[Submission, Reply]],
    on_error_redirect: Optional[str] = None,
) -> werkzeug.Response:
    """Send client contents of ZIP-file *zip_basename*-<timestamp>.zip
    containing *submissions*. The ZIP-file, being a
    :class:`tempfile.NamedTemporaryFile`, is stored on disk only
    temporarily.

    :param str zip_basename: The basename of the ZIP-file download.

    :param list submissions: A list of :class:`models.Submission`s to
                             include in the ZIP-file.
    """
    try:
        zf = Storage.get_default().get_bulk_archive(submissions,
                                                    zip_directory=zip_basename)
    except FileNotFoundError:
        flash(
            ngettext(
                "Your download failed because the file could not be found. An admin can find "
                + "more information in the system and monitoring logs.",
                "Your download failed because a file could not be found. An admin can find "
                + "more information in the system and monitoring logs.",
                len(submissions),
            ),
            "error",
        )
        if on_error_redirect is None:
            on_error_redirect = url_for("main.index")
        return redirect(on_error_redirect)

    attachment_filename = "{}--{}.zip".format(
        zip_basename,
        datetime.now(timezone.utc).strftime("%Y-%m-%d--%H-%M-%S"))

    mark_seen(submissions, g.user)

    return send_file(
        zf.name,
        mimetype="application/zip",
        download_name=attachment_filename,
        as_attachment=True,
    )
Exemplo n.º 22
0
def add_source() -> Tuple[Source, str]:
    """
    Adds a single source.
    """
    codename = PassphraseGenerator.get_default().generate_passphrase()
    source_user = create_source_user(
        db_session=db.session,
        source_passphrase=codename,
        source_app_storage=Storage.get_default(),
    )
    source = source_user.get_db_record()
    source.pending = False
    db.session.commit()

    # Generate source key
    EncryptionManager.get_default().generate_source_key_pair(source_user)

    return source, codename
class UsersView():

    storage = Storage()

    def __init__(self):
        pass

    # === Sing <BEGIN> =======================
    # initial key = the HAS256 result of "userid+pw+time"
    def token_to_global(self, old_token) -> (str, str, str):
        """
        return
        ---
        old_token, new_token, the_global
        """
        new_token = hashlib.sha384(old_token.encode())
        new_token = new_token.hexdigest()

        the_global = hashlib.sha256(new_token.encode())
        the_global = the_global.hexdigest()

        return old_token, new_token, the_global

    def logout(self, the_global):
        self.storage.delete_one(the_global)

    def refresh_global(self, received_global: str, received_time):
        """
        process
        ---
        (1) check match (2) check timeout

        ==> if matched & no timeout

        (3) regennrate & update the global, and (4) send 'OK' http request
        """
        # received_time = datetime.now() + timedelta(minutes=30)

        # regenerate & update the token and global
        _, new_token, new_global = self.token_to_global(received_global)
        self.storage.update_one(received_global, new_global, new_token,
                                received_time)
        print("> Global refreshed")
Exemplo n.º 24
0
def normalize_timestamps(logged_in_source: SourceUser) -> None:
    """
    Update the timestamps on all of the source's submissions. This
    minimizes metadata that could be useful to investigators. See
    #301.
    """
    source_in_db = logged_in_source.get_db_record()
    sub_paths = [
        Storage.get_default().path(logged_in_source.filesystem_id,
                                   submission.filename)
        for submission in source_in_db.submissions
    ]
    if len(sub_paths) > 1:
        args = ["touch", "--no-create"]
        args.extend(sub_paths)
        rc = subprocess.call(args)
        if rc != 0:
            current_app.logger.warning("Couldn't normalize submission "
                                       "timestamps (touch exited with %d)" %
                                       rc)
Exemplo n.º 25
0
    def load_data(self):
        global DATA
        with mock.patch("store.Storage.get_default") as mock_storage_global:
            mock_storage_global.return_value = self.storage
            with self.app.app_context():
                self.create_journalist()
                self.create_source()

                submission_id, submission_filename = self.create_submission()
                reply_id, reply_filename = self.create_reply()

                # we need to actually create files and write data to them so the
                # RQ worker can hash them
                for fn in [submission_filename, reply_filename]:
                    full_path = Storage.get_default().path(
                        self.source_filesystem_id, fn)

                    dirname = path.dirname(full_path)
                    if not path.exists(dirname):
                        os.mkdir(dirname)

                    with io.open(full_path, "wb") as f:
                        f.write(DATA)
Exemplo n.º 26
0
 def __init__(self):
     self.session = requests.Session()
     self.session.headers = {'user-agent': 'shr-podcasts-bot'}
     self.scraper = Scraper(self.session)
     self.parser = Parser(self.session)
     self.storage = Storage()
Exemplo n.º 27
0
class TestHandlers(unittest.TestCase):
    """Test class for testing work of handlers"""
    def setUp(self):
        self.context = {}
        self.store = Storage()
        self.backup = {}
        self.fulfill()
        self.key_hash = set()

    def add_key_to_hash_key(self, arguments):
        self.key_hash.add(
            create_key_part(
                arguments["arguments"].get("first_name"),
                arguments["arguments"].get("last_name"),
                (datetime.datetime.strptime(
                    arguments["arguments"].get("birthday"), "%d.%m.%Y")
                 if arguments["arguments"].get("birthday") else None),
                score_prefix,
            ))

    def fulfill(self):
        for i in range(1, 11):
            interests = random.sample(SOME_INTERESTS, 2)
            key, value = "%s%d" % (interest_prefix, i), json.dumps(interests)
            self.backup.update({i: value})
            self.store._Storage__setkey(key, value, TIME_OF_STORE)

    def tearDown(self):
        self.store.connection.delete_multi(self.key_hash)
        self.store.connection.delete_multi(
            map(lambda key: interest_prefix + str(key), self.backup.keys()))
        self.store.connection.disconnect_all()

    """Test handler MethodRequest"""

    @cases([
        {
            "account": "account",
            "login": "******",
            "token": "token",
            "arguments": {},
            "method": "method"
        },
        {
            "login": "******",
            "token": "token",
            "arguments": {},
            "method": "method"
        },
        {
            "login": "",
            "token": "",
            "arguments": {},
            "method": "method"
        },
    ])
    def test_init_method_request(self, arguments):
        MethodRequest([], self.context, arguments)

    @cases([
        (
            # Test that method cannot be null
            {
                "account": "account",
                "login": "******",
                "token": "token",
                "arguments": {},
                "method": ""
            },
            ['method cannot be null']),
        (
            # Test that request should contain login field
            {
                "token": "token",
                "arguments": {},
                "method": "method"
            },
            ["login is required"]),
        (
            # Test that request should contain arguments field and method is not null
            {
                "login": "",
                "token": "",
                "method": ""
            },
            ["arguments is required", "method cannot be null"]),
    ])
    @mock.patch("api.MethodRequest.raise_error_if_there_are_some")
    def test_bad_init_method_request(self, arguments, errors, _):
        method_request = MethodRequest([], self.context, arguments)
        self.assertSetEqual(set(method_request.errors), set(errors))

    """Test handler ClientsInterestsRequest"""

    @cases([
        (
            # Test there are no problem with initialization and
            # right number in context["nclients"]
            {
                "arguments": {
                    "client_ids": [1, 2, 3]
                },
                "method": "method"
            },
            3,
        ),
        (
            {
                "arguments": {
                    "client_ids": list(range(1000)),
                    "date": "09.11.2017"
                }
            },
            1000,
        ),
        (
            {
                "login": "",
                "token": "",
                "arguments": {
                    "client_ids": [10000]
                }
            },
            1,
        )
    ])
    def test_init_clients_interests_request(self, arguments, nclients):
        ClientsInterestsRequest([], self.context, arguments["arguments"])
        self.assertEqual(self.context["nclients"], nclients)

    # Test exception cases (empty list, wrong format)
    @cases([
        {
            "arguments": {
                "client_ids": []
            },
            "method": "method"
        },
        {
            "arguments": {
                "client_ids": 10
            },
            "method": "method"
        },
        {
            "arguments": {
                "data": "11.11.2111"
            },
            "method": "method"
        },
    ])
    def test_exception_init_clients_interests_request(self, arguments):
        with self.assertRaises(TooMuchErrors):
            ClientsInterestsRequest([], self.context, arguments["arguments"])

    @mock.patch("api.get_interests", func_test_interests)
    def test_process_clients_interests_request(self):
        cli_request = ClientsInterestsRequest(
            self.store, self.context, {"client_ids": list(range(1, 11))})
        result, code = cli_request.process()
        self.assertEqual(code, OK)
        for key, value in result.items():
            self.assertEqual(self.backup[key], json.dumps(value))

    @mock.patch("api.get_interests", func_test_interests)
    def test_process_clients_interests_request_no_keys(self):
        cli_request = ClientsInterestsRequest(
            self.store, self.context, {"client_ids": list(range(12, 21))})

        with self.assertRaises(NoSuchElementError):
            result, _ = cli_request.process()

    """Test handler OnlineScoreRequest"""

    @cases([
        (
            # Test there are no problem with initialization and
            # field case is correct
            {
                "arguments": {
                    "first_name": "first_name",
                    "last_name": "last_name"
                }
            },
            ["first_name", "last_name"],
        ),
        (
            {
                "arguments": {
                    "first_name": "first_name",
                    "last_name": "last_name",
                    "phone": "79178761213"
                }
            },
            ["first_name", "last_name", "phone"],
        ),
        ({
            "arguments": {
                "email": "[email protected]",
                "phone": "79178761213"
            }
        }, ["email", "phone"]),
        ({
            "arguments": {
                "gender": 1,
                "birthday": "11.09.1987",
                "email": "[email protected]"
            }
        }, ["gender", "birthday", "email"]),
    ])
    def test_init_online_score_request(self, arguments, has):
        OnlineScoreRequest(self.store, self.context, arguments["arguments"])
        self.assertSetEqual(set(self.context["has"]), set(has))

    @cases([
        (
            # Test there are no sufficient fields to apply score method
            {
                "arguments": {
                    "first_name": "first_name",
                    "phone": "78923429421"
                }
            }, ),
        ({
            "arguments": {
                "last_name": "last_name",
                "phone": "79178761213"
            }
        }, ),
        ({
            "arguments": {
                "email": "[email protected]",
                "gender": 2
            }
        }, ),
        ({
            "arguments": {
                "birthday": "11.09.1987",
                "email": "[email protected]"
            }
        }, ),
    ])
    def test_bad_init_online_score_request(self, arguments):
        with self.assertRaises(TooLessInformationError):
            OnlineScoreRequest(self.store, self.context,
                               arguments["arguments"])

    @cases([
        (
            # Test correctness of score function
            {
                "arguments": {
                    "first_name": "first_name",
                    "last_name": "last_name"
                }
            },
            .5,
        ),
        (
            {
                "arguments": {
                    "first_name": "N",
                    "last_name": "A",
                    "phone": "79178761213"
                }
            },
            2.,
        ),
        (
            {
                "arguments": {
                    "email": "[email protected]",
                    "phone": "79178761213"
                }
            },
            3.,
        ),
        (
            {
                "arguments": {
                    "gender": 1,
                    "birthday": "11.09.1987",
                    "email": "[email protected]"
                }
            },
            3.,
        ),
    ])
    @mock.patch("api.get_score", func_test_scoring)
    def test_calculate_score_request(self, arguments, score):
        self.add_key_to_hash_key(arguments)
        online_request = OnlineScoreRequest(self.store, self.context,
                                            arguments["arguments"])
        self.assertEqual(online_request.process(), ({"score": score}, OK))

    """Test authorization function"""

    @cases([
        # Right cases
        {
            'login':
            '******',
            'account':
            'Spiderman',
            'token':
            '4de1853f30330c85fb3dc5fc5b1fb2239981e5e0fe1bcfb7137feee75eb9beeef21a63c4652ba576461d7fb60ec9083a7c3cb35345cdf3c798748bd287d975b2'
        },
        {
            'login':
            '******',
            'account':
            'TV actress',
            'token':
            'dce3e3f520c9294c4ac7fd6e3434be2ed5de97423350f7c509f6321dc6c8a38fa386b1d8a9cbd2f27c6f9c8164e3e11fd369547d988123f31132a69c6e986de7'
        },
        {
            'login':
            '******',
            'account':
            'Houston Rockets',
            'token':
            'f203215d025be91486e287252ff3895ebecd76f1f280faf7d2fee20610fad104dd60975cceadd4b30e40afeea70c0d66cbe5581edc7802674c07bad21c39bb5e'
        },
        {
            'login': '******',
            'account': 'admin',
            'token': get_current_admin_token()
        },
    ])
    def test_right_authorization(self, storage):
        auth_request = create_request(storage)["body"]
        self.assertTrue(check_auth(auth_request))

    @cases([
        {
            'login':
            '******',
            'account':
            'Hulk',
            'token':
            '4de1853f30330c85fb3dc5fc5b1fb2239981e5e0fe1bcfb7137feee75eb9beeef21a63c4652ba576461d7fb60ec9083a7c3cb35345cdf3c798748bd287d975b2'
        },
        {
            'login':
            '******',
            'account':
            'TV actress',
            'token':
            'dce3e3f520c9294c4ac7fd6e3434be2ed5de97423350f7c509f6321dc6c8a38fa386b1d8a9cbd2f27c6f9c8164e3e11fd369547d988123f31132a69c6e986de7'
        },
        {
            'login': '******',
            'account': 'Houston Rockets',
            'token': 'YaoMingFromChinaWhoIsVeryTallButCantBeAuthorizedThisTime'
        },
        {
            'login': '******',
            'account': 'admin',
            'token': 'IDontNeedTokenToAuthenticateIHackIt!!'
        },
    ])
    def test_bad_authorization(self, storage):
        auth_request = create_request(storage)["body"]
        self.assertFalse(check_auth(auth_request))

    """Test method_handler function which routes handlers"""

    @cases([
        {
            'login':
            '******',
            'account':
            'Hulk',
            'token':
            '4de1853f30330c85fb3dc5fc5b1fb2239981e5e0fe1bcfb7137feee75eb9beeef21a63c4652ba576461d7fb60ec9083a7c3cb35345cdf3c798748bd287d975b2'
        },
        {
            'login':
            '******',
            'account':
            'TV actress',
            'token':
            'dce3e3f520c9294c4ac7fd6e3434be2ed5de97423350f7c509f6321dc6c8a38fa386b1d8a9cbd2f27c6f9c8164e3e11fd369547d988123f31132a69c6e986de7'
        },
        {
            'login': '******',
            'account': 'Houston Rockets',
            'token': 'YaoMingFromChinaWhoIsVeryTallButCantBeAuthorizedThisTime'
        },
        {
            'login': '******',
            'account': 'admin',
            'token': 'IDontNeedTokenToAuthenticateIHackIt!!'
        },
    ])
    @mock.patch("api.MethodRequest", lambda _x, _y, body: body)
    def test_forbidden_access(self, storage):
        request = create_request(storage)
        self.assertEqual(method_handler(request, self.context, []),
                         (None, FORBIDDEN))

    @cases([
        {
            'login':
            '******',
            'account':
            'Spiderman',
            'token':
            '4de1853f30330c85fb3dc5fc5b1fb2239981e5e0fe1bcfb7137feee75eb9beeef21a63c4652ba576461d7fb60ec9083a7c3cb35345cdf3c798748bd287d975b2'
        },
        {
            'login':
            '******',
            'account':
            'TV actress',
            'token':
            'dce3e3f520c9294c4ac7fd6e3434be2ed5de97423350f7c509f6321dc6c8a38fa386b1d8a9cbd2f27c6f9c8164e3e11fd369547d988123f31132a69c6e986de7'
        },
        {
            'login':
            '******',
            'account':
            'Houston Rockets',
            'token':
            'f203215d025be91486e287252ff3895ebecd76f1f280faf7d2fee20610fad104dd60975cceadd4b30e40afeea70c0d66cbe5581edc7802674c07bad21c39bb5e'
        },
        {
            'login': '******',
            'account': 'admin',
            'token': get_current_admin_token()
        },
    ])
    @mock.patch("api.MethodRequest", lambda _x, _y, body: body)
    def test_no_method_in_request(self, storage):
        request = create_request(storage)
        with self.assertRaises(NoMethodError):
            method_handler(request, self.context, [])

    @cases([
        {
            'login':
            '******',
            'account':
            'Spiderman',
            'method':
            'online_score',
            'token':
            '4de1853f30330c85fb3dc5fc5b1fb2239981e5e0fe1bcfb7137feee75eb9beeef21a63c4652ba576461d7fb60ec9083a7c3cb35345cdf3c798748bd287d975b2'
        },
        {
            'login':
            '******',
            'account':
            'TV actress',
            'method':
            'clients_interests',
            'token':
            'dce3e3f520c9294c4ac7fd6e3434be2ed5de97423350f7c509f6321dc6c8a38fa386b1d8a9cbd2f27c6f9c8164e3e11fd369547d988123f31132a69c6e986de7'
        },
        {
            'login':
            '******',
            'account':
            'Houston Rockets',
            'method':
            'online_score',
            'token':
            'f203215d025be91486e287252ff3895ebecd76f1f280faf7d2fee20610fad104dd60975cceadd4b30e40afeea70c0d66cbe5581edc7802674c07bad21c39bb5e'
        },
        {
            'login': '******',
            'account': 'admin',
            'method': 'clients_interests',
            'token': get_current_admin_token()
        },
    ])
    @mock.patch("api.MethodRequest", lambda _x, _y, body: body)
    def test_no_arguments_in_request(self, storage):
        request = create_request(storage)
        with self.assertRaises(NoArgumentsError):
            method_handler(request, self.context, [])

    """Test partial filled requests, which should cause errors"""

    @cases([
        {
            "body": {}
        },
        {
            "body": {
                "login": "******"
            }
        },
        {
            "body": {
                "login": "******",
                "token": "123"
            }
        },
        {
            "body": {
                "login": "******",
                "token": "123",
                "method": "method"
            }
        },
    ])
    def test_empty_or_partial_filled_request(self, request):
        with self.assertRaises(TooMuchErrors):
            method_handler(request, self.context, self.store)

    """END"""
Exemplo n.º 28
0
def create_app(config: SDConfig) -> Flask:
    app = Flask(__name__,
                template_folder=config.SOURCE_TEMPLATES_DIR,
                static_folder=path.join(config.SECUREDROP_ROOT, 'static'))
    app.request_class = RequestThatSecuresFileUploads
    app.config.from_object(config.SOURCE_APP_FLASK_CONFIG_CLS)

    # The default CSRF token expiration is 1 hour. Since large uploads can
    # take longer than an hour over Tor, we increase the valid window to 24h.
    app.config['WTF_CSRF_TIME_LIMIT'] = 60 * 60 * 24
    CSRFProtect(app)

    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    app.config['SQLALCHEMY_DATABASE_URI'] = config.DATABASE_URI
    db.init_app(app)

    # TODO: Attaching a Storage dynamically like this disables all type checking (and
    # breaks code analysis tools) for code that uses current_app.storage; it should be refactored
    app.storage = Storage(config.STORE_DIR,
                          config.TEMP_DIR,
                          config.JOURNALIST_KEY)

    # TODO: Attaching a CryptoUtil dynamically like this disables all type checking (and
    # breaks code analysis tools) for code that uses current_app.storage; it should be refactored
    app.crypto_util = CryptoUtil(
        scrypt_params=config.SCRYPT_PARAMS,
        scrypt_id_pepper=config.SCRYPT_ID_PEPPER,
        scrypt_gpg_pepper=config.SCRYPT_GPG_PEPPER,
        securedrop_root=config.SECUREDROP_ROOT,
        word_list=config.WORD_LIST,
        nouns_file=config.NOUNS,
        adjectives_file=config.ADJECTIVES,
        gpg_key_dir=config.GPG_KEY_DIR,
    )

    @app.errorhandler(CSRFError)
    def handle_csrf_error(e: CSRFError) -> werkzeug.Response:
        msg = render_template('session_timeout.html')
        session.clear()
        flash(Markup(msg), "important")
        return redirect(url_for('main.index'))

    assets = Environment(app)
    app.config['assets'] = assets

    i18n.setup_app(config, app)

    app.jinja_env.trim_blocks = True
    app.jinja_env.lstrip_blocks = True
    app.jinja_env.globals['version'] = version.__version__
    # Exported to source templates for being included in instructions
    app.jinja_env.globals['submission_key_fpr'] = config.JOURNALIST_KEY
    app.jinja_env.filters['rel_datetime_format'] = \
        template_filters.rel_datetime_format
    app.jinja_env.filters['nl2br'] = evalcontextfilter(template_filters.nl2br)
    app.jinja_env.filters['filesizeformat'] = template_filters.filesizeformat

    for module in [main, info, api]:
        app.register_blueprint(module.make_blueprint(config))  # type: ignore

    @app.before_request
    @ignore_static
    def setup_i18n() -> None:
        """Store i18n-related values in Flask's special g object"""
        g.locale = i18n.get_locale(config)
        g.text_direction = i18n.get_text_direction(g.locale)
        g.html_lang = i18n.locale_to_rfc_5646(g.locale)
        g.locales = i18n.get_locale2name()

    @app.before_request
    @ignore_static
    def check_tor2web() -> None:
        # ignore_static here so we only flash a single message warning
        # about Tor2Web, corresponding to the initial page load.
        if 'X-tor2web' in request.headers:
            flash(Markup(gettext(
                '<strong>WARNING:&nbsp;</strong> '
                'You appear to be using Tor2Web. '
                'This <strong>&nbsp;does not&nbsp;</strong> '
                'provide anonymity. '
                '<a href="{url}">Why is this dangerous?</a>')
                .format(url=url_for('info.tor2web_warning'))),
                "banner-warning")

    @app.before_request
    @ignore_static
    def load_instance_config() -> None:
        app.instance_config = InstanceConfig.get_current()

    @app.before_request
    @ignore_static
    def setup_g() -> Optional[werkzeug.Response]:
        """Store commonly used values in Flask's special g object"""

        if 'expires' in session and datetime.utcnow() >= session['expires']:
            msg = render_template('session_timeout.html')

            # Show expiration message only if the user was
            # either in the codename generation flow or logged in
            show_expiration_message = any([
                session.get('show_expiration_message'),
                logged_in(),
                was_in_generate_flow(),
            ])

            # clear the session after we render the message so it's localized
            session.clear()

            # Persist this properety across sessions to distinguish users whose sessions expired
            # from users who never logged in or generated a codename
            session['show_expiration_message'] = show_expiration_message

            # Redirect to index with flashed message
            if session['show_expiration_message']:
                flash(Markup(msg), "important")
            return redirect(url_for('main.index'))

        session['expires'] = datetime.utcnow() + \
            timedelta(minutes=getattr(config,
                                      'SESSION_EXPIRATION_MINUTES',
                                      120))

        # ignore_static here because `crypto_util.hash_codename` is scrypt
        # (very time consuming), and we don't need to waste time running if
        # we're just serving a static resource that won't need to access
        # these common values.
        if logged_in():
            g.codename = session['codename']
            g.filesystem_id = app.crypto_util.hash_codename(g.codename)
            try:
                g.source = Source.query \
                            .filter(Source.filesystem_id == g.filesystem_id) \
                            .filter_by(deleted_at=None) \
                            .one()
            except NoResultFound as e:
                app.logger.error(
                    "Found no Sources when one was expected: %s" %
                    (e,))
                del session['logged_in']
                del session['codename']
                return redirect(url_for('main.index'))
            g.loc = app.storage.path(g.filesystem_id)
        return None

    @app.errorhandler(404)
    def page_not_found(error: werkzeug.exceptions.HTTPException) -> Tuple[str, int]:
        return render_template('notfound.html'), 404

    @app.errorhandler(500)
    def internal_error(error: werkzeug.exceptions.HTTPException) -> Tuple[str, int]:
        return render_template('error.html'), 500

    return app
Exemplo n.º 29
0
def create_app(config):
    # type: (SDConfig) -> Flask
    app = Flask(__name__,
                template_folder=config.SOURCE_TEMPLATES_DIR,
                static_folder=path.join(config.SECUREDROP_ROOT, 'static'))
    app.request_class = RequestThatSecuresFileUploads
    app.config.from_object(config.SourceInterfaceFlaskConfig)  # type: ignore
    app.sdconfig = config

    # The default CSRF token expiration is 1 hour. Since large uploads can
    # take longer than an hour over Tor, we increase the valid window to 24h.
    app.config['WTF_CSRF_TIME_LIMIT'] = 60 * 60 * 24
    CSRFProtect(app)

    if config.DATABASE_ENGINE == "sqlite":
        db_uri = (config.DATABASE_ENGINE + ":///" + config.DATABASE_FILE)
    else:
        db_uri = (config.DATABASE_ENGINE + '://' + config.DATABASE_USERNAME +
                  ':' + config.DATABASE_PASSWORD + '@' + config.DATABASE_HOST +
                  '/' + config.DATABASE_NAME)
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    app.config['SQLALCHEMY_DATABASE_URI'] = db_uri
    db.init_app(app)

    app.storage = Storage(config.STORE_DIR, config.TEMP_DIR,
                          config.JOURNALIST_KEY)

    app.crypto_util = CryptoUtil(
        scrypt_params=config.SCRYPT_PARAMS,
        scrypt_id_pepper=config.SCRYPT_ID_PEPPER,
        scrypt_gpg_pepper=config.SCRYPT_GPG_PEPPER,
        securedrop_root=config.SECUREDROP_ROOT,
        word_list=config.WORD_LIST,
        nouns_file=config.NOUNS,
        adjectives_file=config.ADJECTIVES,
        gpg_key_dir=config.GPG_KEY_DIR,
    )

    @app.errorhandler(CSRFError)
    def handle_csrf_error(e):
        msg = render_template('session_timeout.html')
        session.clear()
        flash(Markup(msg), "important")
        return redirect(url_for('main.index'))

    assets = Environment(app)
    app.config['assets'] = assets

    i18n.setup_app(config, app)

    app.jinja_env.trim_blocks = True
    app.jinja_env.lstrip_blocks = True
    app.jinja_env.globals['version'] = version.__version__
    if getattr(config, 'CUSTOM_HEADER_IMAGE', None):
        app.jinja_env.globals['header_image'] = \
            config.CUSTOM_HEADER_IMAGE  # type: ignore
        app.jinja_env.globals['use_custom_header_image'] = True
    else:
        app.jinja_env.globals['header_image'] = 'logo.png'
        app.jinja_env.globals['use_custom_header_image'] = False

    app.jinja_env.filters['rel_datetime_format'] = \
        template_filters.rel_datetime_format
    app.jinja_env.filters['nl2br'] = evalcontextfilter(template_filters.nl2br)
    app.jinja_env.filters['filesizeformat'] = template_filters.filesizeformat

    for module in [main, info, api]:
        app.register_blueprint(module.make_blueprint(config))  # type: ignore

    @app.before_request
    @ignore_static
    def setup_i18n():
        """Store i18n-related values in Flask's special g object"""
        g.locale = i18n.get_locale(config)
        g.text_direction = i18n.get_text_direction(g.locale)
        g.html_lang = i18n.locale_to_rfc_5646(g.locale)
        g.locales = i18n.get_locale2name()

    @app.before_request
    @ignore_static
    def check_tor2web():
        # ignore_static here so we only flash a single message warning
        # about Tor2Web, corresponding to the initial page load.
        if 'X-tor2web' in request.headers:
            flash(
                Markup(
                    gettext(
                        '<strong>WARNING:&nbsp;</strong> '
                        'You appear to be using Tor2Web. '
                        'This <strong>&nbsp;does not&nbsp;</strong> '
                        'provide anonymity. '
                        '<a href="{url}">Why is this dangerous?</a>').format(
                            url=url_for('info.tor2web_warning'))),
                "banner-warning")

    @app.before_request
    @ignore_static
    def load_instance_config():
        app.instance_config = InstanceConfig.get_current()

    @app.before_request
    @ignore_static
    def setup_g():
        """Store commonly used values in Flask's special g object"""

        if 'expires' in session and datetime.utcnow() >= session['expires']:
            msg = render_template('session_timeout.html')

            # clear the session after we render the message so it's localized
            session.clear()

            # Redirect to index with flashed message
            flash(Markup(msg), "important")
            return redirect(url_for('main.index'))

        session['expires'] = datetime.utcnow() + \
            timedelta(minutes=getattr(config,
                                      'SESSION_EXPIRATION_MINUTES',
                                      120))

        # ignore_static here because `crypto_util.hash_codename` is scrypt
        # (very time consuming), and we don't need to waste time running if
        # we're just serving a static resource that won't need to access
        # these common values.
        if logged_in():
            g.codename = session['codename']
            g.filesystem_id = app.crypto_util.hash_codename(g.codename)
            try:
                g.source = Source.query \
                            .filter(Source.filesystem_id == g.filesystem_id) \
                            .one()
            except NoResultFound as e:
                app.logger.error("Found no Sources when one was expected: %s" %
                                 (e, ))
                del session['logged_in']
                del session['codename']
                return redirect(url_for('main.index'))
            g.loc = app.storage.path(g.filesystem_id)

    @app.errorhandler(404)
    def page_not_found(error):
        return render_template('notfound.html'), 404

    @app.errorhandler(500)
    def internal_error(error):
        return render_template('error.html'), 500

    return app
Exemplo n.º 30
0
class TestFunctionalOfApi(unittest.TestCase):
    def setUp(self):
        self.context = {}
        self.store = Storage()
        self.backup = {}
        self.fulfill()
        self.key_hash = set()

    def add_key_to_hash_key(self, arguments):
        self.key_hash.add(
            create_key_part(
                arguments["arguments"].get("first_name"),
                arguments["arguments"].get("last_name"),
                (datetime.datetime.strptime(
                    arguments["arguments"].get("birthday"), "%d.%m.%Y")
                 if arguments["arguments"].get("birthday") else None),
                score_prefix,
            ))

    def fulfill(self):
        for i, interests in enumerate(POPULAR_INTERESTS):
            key, value = "%s%d" % (interest_prefix, i), json.dumps(interests)
            self.backup.update({i: value})
            self.store._Storage__setkey(key, value, TIME_OF_STORE)

    def tearDown(self):
        self.store.connection.delete_multi(self.key_hash)
        self.store.connection.delete_multi(
            map(lambda key: interest_prefix + str(key), self.backup.keys()))
        self.store.connection.disconnect_all()

    """Functional test to test correctness of interests method request"""

    @cases([
        (
            {
                'login':
                '******',
                'account':
                'Spiderman',
                'method':
                'clients_interests',
                "arguments": {
                    "client_ids": [0, 1, 2, 3]
                },
                'token':
                '4de1853f30330c85fb3dc5fc5b1fb2239981e5e0fe1bcfb7137feee75eb9beeef21a63c4652ba576461d7fb60ec9083a7c3cb35345cdf3c798748bd287d975b2'
            },
            dict(zip([0, 1, 2, 3], POPULAR_INTERESTS[0:4])),
        ),
        (
            {
                'login': '******',
                'account': 'admin',
                'method': 'clients_interests',
                "arguments": {
                    "client_ids": [0, 1, 2]
                },
                'token': get_current_admin_token()
            },
            dict(zip([0, 1, 2], POPULAR_INTERESTS[0:3])),
        ),
    ])
    @mock.patch("api.get_interests", func_test_interests)
    def test_interest_handler_is_correct(self, storage, answers):
        request = {"body": storage}
        response, code = method_handler(request, self.context, self.store)
        self.assertEqual(code, OK)
        for key, value in response.items():
            self.assertEqual(answers[key], value)

    """Functional test to test correctness of online score method request"""

    @cases([
        (
            {
                'login':
                '******',
                'account':
                'Spiderman',
                'method':
                'online_score',
                'arguments': {
                    'first_name': 'Peter',
                    'last_name': 'Parker'
                },
                'token':
                '4de1853f30330c85fb3dc5fc5b1fb2239981e5e0fe1bcfb7137feee75eb9beeef21a63c4652ba576461d7fb60ec9083a7c3cb35345cdf3c798748bd287d975b2'
            },
            .5,
        ),
        (
            {
                'login':
                '******',
                'account':
                'TV actress',
                'method':
                'online_score',
                'arguments': {
                    'first_name': 'Mary',
                    'last_name': 'Jane',
                    'gender': 2,
                    'birthday': '11.10.1965'
                },
                'token':
                'dce3e3f520c9294c4ac7fd6e3434be2ed5de97423350f7c509f6321dc6c8a38fa386b1d8a9cbd2f27c6f9c8164e3e11fd369547d988123f31132a69c6e986de7'
            },
            2.,
        ),
        (
            {
                'login':
                '******',
                'account':
                'Houston Rockets',
                'method':
                'online_score',
                'arguments': {
                    'height': 225,
                    'weight': '140',
                    'email': '*****@*****.**',
                    'phone': '77777777777'
                },
                'token':
                'f203215d025be91486e287252ff3895ebecd76f1f280faf7d2fee20610fad104dd60975cceadd4b30e40afeea70c0d66cbe5581edc7802674c07bad21c39bb5e'
            },
            3.,
        ),
        (
            {
                'login': '******',
                'account': 'admin',
                'method': 'online_score',
                'arguments': {},
                'token': get_current_admin_token()
            },
            42,
        )
    ])
    @mock.patch("api.get_score", func_test_scoring)
    def test_score_handler_is_correct(self, storage, score):
        request = {"body": storage}
        self.add_key_to_hash_key(request["body"])
        response, code = method_handler(request, self.context, self.store)
        self.assertEqual(code, OK)
        self.assertEqual(response, {"score": score})
Exemplo n.º 31
0
def create_app(config: 'SDConfig') -> Flask:
    app = Flask(__name__,
                template_folder=config.JOURNALIST_TEMPLATES_DIR,
                static_folder=path.join(config.SECUREDROP_ROOT, 'static'))

    app.config.from_object(config.JOURNALIST_APP_FLASK_CONFIG_CLS)
    app.session_interface = JournalistInterfaceSessionInterface()

    csrf = CSRFProtect(app)
    Environment(app)

    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    app.config['SQLALCHEMY_DATABASE_URI'] = config.DATABASE_URI
    db.init_app(app)

    v2_enabled = path.exists(
        path.join(config.SECUREDROP_DATA_ROOT, 'source_v2_url'))
    v3_enabled = path.exists(
        path.join(config.SECUREDROP_DATA_ROOT, 'source_v3_url'))
    app.config.update(V2_ONION_ENABLED=v2_enabled, V3_ONION_ENABLED=v3_enabled)

    # TODO: Attaching a Storage dynamically like this disables all type checking (and
    # breaks code analysis tools) for code that uses current_app.storage; it should be refactored
    app.storage = Storage(config.STORE_DIR, config.TEMP_DIR,
                          config.JOURNALIST_KEY)

    # TODO: Attaching a CryptoUtil dynamically like this disables all type checking (and
    # breaks code analysis tools) for code that uses current_app.storage; it should be refactored
    app.crypto_util = CryptoUtil(
        scrypt_params=config.SCRYPT_PARAMS,
        scrypt_id_pepper=config.SCRYPT_ID_PEPPER,
        scrypt_gpg_pepper=config.SCRYPT_GPG_PEPPER,
        securedrop_root=config.SECUREDROP_ROOT,
        word_list=config.WORD_LIST,
        nouns_file=config.NOUNS,
        adjectives_file=config.ADJECTIVES,
        gpg_key_dir=config.GPG_KEY_DIR,
    )

    @app.errorhandler(CSRFError)
    def handle_csrf_error(e: CSRFError) -> 'Response':
        # render the message first to ensure it's localized.
        msg = gettext('You have been logged out due to inactivity.')
        session.clear()
        flash(msg, 'error')
        return redirect(url_for('main.login'))

    def _handle_http_exception(
        error: 'HTTPException'
    ) -> 'Tuple[Union[Response, str], Optional[int]]':
        # Workaround for no blueprint-level 404/5 error handlers, see:
        # https://github.com/pallets/flask/issues/503#issuecomment-71383286
        handler = list(app.error_handler_spec['api'][error.code].values())[0]
        if request.path.startswith('/api/') and handler:
            return handler(error)

        return render_template('error.html', error=error), error.code

    for code in default_exceptions:
        app.errorhandler(code)(_handle_http_exception)

    i18n.setup_app(config, app)

    app.jinja_env.trim_blocks = True
    app.jinja_env.lstrip_blocks = True
    app.jinja_env.globals['version'] = version.__version__
    app.jinja_env.filters['rel_datetime_format'] = \
        template_filters.rel_datetime_format
    app.jinja_env.filters['filesizeformat'] = template_filters.filesizeformat

    @app.before_first_request
    def expire_blacklisted_tokens() -> None:
        cleanup_expired_revoked_tokens()

    @app.before_request
    def load_instance_config() -> None:
        app.instance_config = InstanceConfig.get_current()

    @app.before_request
    def setup_g() -> 'Optional[Response]':
        """Store commonly used values in Flask's special g object"""
        if 'expires' in session and datetime.utcnow() >= session['expires']:
            session.clear()
            flash(gettext('You have been logged out due to inactivity.'),
                  'error')

        uid = session.get('uid', None)
        if uid:
            user = Journalist.query.get(uid)
            if user and 'nonce' in session and \
               session['nonce'] != user.session_nonce:
                session.clear()
                flash(
                    gettext('You have been logged out due to password change'),
                    'error')

        session['expires'] = datetime.utcnow() + \
            timedelta(minutes=getattr(config,
                                      'SESSION_EXPIRATION_MINUTES',
                                      120))

        # Work around https://github.com/lepture/flask-wtf/issues/275
        # -- after upgrading from Python 2 to Python 3, any existing
        # session's csrf_token value will be retrieved as bytes,
        # causing a TypeError. This simple fix, deleting the existing
        # token, was suggested in the issue comments. This code will
        # be safe to remove after Python 2 reaches EOL in 2020, and no
        # supported SecureDrop installations can still have this
        # problem.
        if sys.version_info.major > 2 and type(
                session.get('csrf_token')) is bytes:
            del session['csrf_token']

        uid = session.get('uid', None)
        if uid:
            g.user = Journalist.query.get(uid)

        g.locale = i18n.get_locale(config)
        g.text_direction = i18n.get_text_direction(g.locale)
        g.html_lang = i18n.locale_to_rfc_5646(g.locale)
        g.locales = i18n.get_locale2name()

        if not app.config['V3_ONION_ENABLED'] or app.config['V2_ONION_ENABLED']:
            g.show_v2_onion_eol_warning = True

        if request.path.split('/')[1] == 'api':
            pass  # We use the @token_required decorator for the API endpoints
        else:  # We are not using the API
            if request.endpoint not in _insecure_views and not logged_in():
                return redirect(url_for('main.login'))

        if request.method == 'POST':
            filesystem_id = request.form.get('filesystem_id')
            if filesystem_id:
                g.filesystem_id = filesystem_id
                g.source = get_source(filesystem_id)

        return None

    app.register_blueprint(main.make_blueprint(config))
    app.register_blueprint(account.make_blueprint(config),
                           url_prefix='/account')
    app.register_blueprint(admin.make_blueprint(config), url_prefix='/admin')
    app.register_blueprint(col.make_blueprint(config), url_prefix='/col')
    api_blueprint = api.make_blueprint(config)
    app.register_blueprint(api_blueprint, url_prefix='/api/v1')
    csrf.exempt(api_blueprint)

    return app
Exemplo n.º 32
0
 def setUp(self):
     self.context = {}
     self.store = Storage()
     self.backup = {}
     self.fulfill()
     self.key_hash = set()