Skip to content

Commit 69e3dec

Browse files
authored
Merge pull request #7712 from freedomofpress/apiv2_switch
Add config flag for the v2 api, disabling it in prod for now
2 parents d466bea + 82e40bb commit 69e3dec

File tree

5 files changed

+56
-4
lines changed

5 files changed

+56
-4
lines changed

securedrop/config.py.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ JOURNALIST_KEY = '{{ securedrop_app_gpg_fingerprint }}'
4545
# encrypted submissions.
4646
SECUREDROP_DATA_ROOT = '/var/lib/securedrop'
4747

48+
# Feature flags for journalist interface services, with default production values.
49+
# Only the v2 api is feature-flagged for now.
50+
51+
V2_API_ENABLED = False
52+
53+
4854
# Modify configuration for alternative environments
4955
env = os.environ.get('SECUREDROP_ENV') or 'prod'
5056

@@ -57,11 +63,13 @@ elif env == 'dev':
5763
# Use MAX_CONTENT_LENGTH to mimic the behavior of Apache's LimitRequestBody
5864
# in the development environment. See #1714.
5965
FlaskConfig.MAX_CONTENT_LENGTH = 524288000
66+
V2_API_ENABLED = True
6067
elif env == 'test':
6168
FlaskConfig.TESTING = True
6269
# Disable CSRF checks to make writing tests easier
6370
FlaskConfig.WTF_CSRF_ENABLED = False
6471
SECUREDROP_DATA_ROOT = '/tmp/securedrop'
72+
V2_API_ENABLED = True
6573

6674
# The following configuration is dependent on SECUREDROP_DATA_ROOT
6775

securedrop/journalist_app/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ def setup_g() -> Optional[Response]:
148148
api_blueprint = api.make_blueprint()
149149
app.register_blueprint(api_blueprint, url_prefix="/api/v1")
150150
csrf.exempt(api_blueprint)
151-
app.register_blueprint(api2.blp, url_prefix="/api/v2")
152-
csrf.exempt(api2.blp)
151+
if config.V2_API_ENABLED:
152+
app.register_blueprint(api2.blp, url_prefix="/api/v2")
153+
csrf.exempt(api2.blp)
153154

154155
return app

securedrop/sdconfig.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ class SecureDropConfig:
7373

7474
REDIS_PASSWORD: str
7575

76+
V2_API_ENABLED: bool
77+
7678
env: str = "prod"
7779

7880
@property
@@ -127,6 +129,8 @@ def _parse_config_from_file(config_module_name: str) -> SecureDropConfig:
127129

128130
env = getattr(config_from_local_file, "env", "prod")
129131

132+
final_v2_api_enabled = getattr(config_from_local_file, "V2_API_ENABLED", False)
133+
130134
try:
131135
final_securedrop_root = Path(config_from_local_file.SECUREDROP_ROOT)
132136
except AttributeError:
@@ -222,4 +226,5 @@ def _parse_config_from_file(config_module_name: str) -> SecureDropConfig:
222226
SESSION_EXPIRATION_MINUTES=final_sess_expiration_mins,
223227
RQ_WORKER_NAME=final_worker_name,
224228
REDIS_PASSWORD=final_redis_password,
229+
V2_API_ENABLED=final_v2_api_enabled,
225230
)

securedrop/tests/factories.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def create(
6262
SUPPORTED_LOCALES: Optional[List[str]] = None,
6363
DEFAULT_LOCALE: str = "en_US",
6464
TRANSLATION_DIRS: Path = DEFAULT_SECUREDROP_ROOT / "translations",
65+
V2_API_ENABLED: bool = True,
6566
) -> SecureDropConfig:
6667
"""Create a securedrop config suitable for the unit tests.
6768
@@ -98,6 +99,7 @@ def create(
9899
JOURNALIST_TEMPLATES_DIR=DEFAULT_SECUREDROP_ROOT / "journalist_templates",
99100
DEFAULT_LOCALE=DEFAULT_LOCALE,
100101
REDIS_PASSWORD=REDIS_PASSWORD,
102+
V2_API_ENABLED=V2_API_ENABLED,
101103
)
102104

103105
# Delete any previous/existing DB and initialize a new one

securedrop/tests/test_journalist_api2.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,23 @@
33
from copy import deepcopy
44
from dataclasses import asdict
55
from datetime import UTC, datetime
6+
from pathlib import Path
7+
from typing import Tuple
8+
from uuid import uuid4
69

710
import pytest
811
from flask import url_for
912
from flask_sqlalchemy import get_debug_queries
10-
from journalist_app import api2
13+
from journalist_app import api2, create_app
1114
from journalist_app.api2.shared import json_version
1215
from journalist_app.api2.types import Event, EventType, ItemTarget, SourceTarget
1316
from models import Reply, Source, SourceStar, Submission, db
1417
from sqlalchemy.orm.exc import MultipleResultsFound
15-
from tests.utils import ascii_armor, decrypt_as_journalist
18+
from tests.factories import SecureDropConfigFactory
19+
from tests.utils import ascii_armor, decrypt_as_journalist, i18n
1620
from tests.utils.api_helper import get_api_headers
1721
from tests.utils.db_helper import init_source, submit
22+
from werkzeug.routing import BuildError
1823

1924

2025
def filtered_queries():
@@ -56,6 +61,37 @@ def test_json_version():
5661
assert version1 == version2
5762

5863

64+
@pytest.mark.parametrize(
65+
"endpoint",
66+
[
67+
"api2.index",
68+
"api2.data",
69+
],
70+
)
71+
def test_api2_not_available_when_disabled(
72+
setup_journalist_key_and_gpg_folder: Tuple[str, Path],
73+
setup_rqworker: Tuple[str, str],
74+
endpoint: str,
75+
) -> None:
76+
journalist_key_fingerprint, gpg_key_dir = setup_journalist_key_and_gpg_folder
77+
worker_name, _ = setup_rqworker
78+
config_without_v2api = SecureDropConfigFactory.create(
79+
SECUREDROP_DATA_ROOT=Path(f"/tmp/sd-tests/conftest-{uuid4()}"),
80+
GPG_KEY_DIR=gpg_key_dir,
81+
JOURNALIST_KEY=journalist_key_fingerprint,
82+
SUPPORTED_LOCALES=i18n.get_test_locales(),
83+
RQ_WORKER_NAME=worker_name,
84+
V2_API_ENABLED=False,
85+
)
86+
app = create_app(config_without_v2api)
87+
app.config["SERVER_NAME"] = "localhost.localdomain"
88+
89+
with app.app_context():
90+
with app.test_client() as client_app:
91+
with pytest.raises(BuildError):
92+
client_app.get(url_for(endpoint))
93+
94+
5995
@pytest.mark.parametrize(
6096
("endpoint", "kwargs"),
6197
[

0 commit comments

Comments
 (0)