Make CORS allow origins opt-in by default (instead of opt-out and having '*' as the default).

Ensure that empty string for --allow-origins equates to cors not enabled.
Add tests for CORS CLI option.
pull/2807/head
derekpierre 2021-10-05 11:54:46 -04:00
parent 5b60b2a0ab
commit 9853509716
5 changed files with 138 additions and 30 deletions

View File

@ -16,7 +16,8 @@ services:
- ~/.local/share/nucypher:/nucypher
command: ["nucypher", "porter", "run",
"--provider", "${WEB3_PROVIDER_URI}",
"--network", "${NUCYPHER_NETWORK}"]
"--network", "${NUCYPHER_NETWORK}",
"--allow-origins", "${PORTER_CORS_ALLOW_ORIGINS}"] # empty string if env var not defined which translates to CORS not enabled by default
porter-https:
restart: on-failure
@ -33,7 +34,8 @@ services:
"--provider", "${WEB3_PROVIDER_URI}",
"--network", "${NUCYPHER_NETWORK}",
"--tls-key-filepath", "/etc/porter/tls/key.pem",
"--tls-certificate-filepath", "/etc/porter/tls/cert.pem"]
"--tls-certificate-filepath", "/etc/porter/tls/cert.pem",
"--allow-origins", "${PORTER_CORS_ALLOW_ORIGINS}"] # empty string if env var not defined which translates to CORS not enabled by default
porter-https-auth:
restart: on-failure
@ -52,4 +54,5 @@ services:
"--network", "${NUCYPHER_NETWORK}",
"--tls-key-filepath", "/etc/porter/tls/key.pem",
"--tls-certificate-filepath", "/etc/porter/tls/cert.pem",
"--basic-auth-filepath", "/etc/porter/auth/htpasswd"]
"--basic-auth-filepath", "/etc/porter/auth/htpasswd",
"--allow-origins", "${PORTER_CORS_ALLOW_ORIGINS}"] # empty string if env var not defined which translates to CORS not enabled by default

View File

@ -21,8 +21,13 @@ import click
from nucypher.blockchain.eth.networks import NetworksInventory
from nucypher.characters.lawful import Ursula
from nucypher.cli.config import group_general_config
from nucypher.cli.literature import BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED, PORTER_RUN_MESSAGE, \
BASIC_AUTH_REQUIRES_HTTPS
from nucypher.cli.literature import (
PORTER_BASIC_AUTH_ENABLED,
PORTER_BASIC_AUTH_REQUIRES_HTTPS,
PORTER_BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED,
PORTER_CORS_ALLOWED_ORIGINS,
PORTER_RUN_MESSAGE,
)
from nucypher.cli.options import (
option_network,
option_provider_uri,
@ -34,7 +39,6 @@ from nucypher.cli.options import (
from nucypher.cli.types import NETWORK_PORT
from nucypher.cli.utils import setup_emitter, get_registry
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.utilities.porter.control.interfaces import PorterInterface
from nucypher.utilities.porter.porter import Porter
@ -58,7 +62,7 @@ def porter():
@click.option('--tls-certificate-filepath', help="Pre-signed TLS certificate filepath", type=click.Path(dir_okay=False, exists=True, path_type=Path))
@click.option('--tls-key-filepath', help="TLS private key filepath", type=click.Path(dir_okay=False, exists=True, path_type=Path))
@click.option('--basic-auth-filepath', help="htpasswd filepath for basic authentication", type=click.Path(dir_okay=False, exists=True, resolve_path=True, path_type=Path))
@click.option('--allow-origins', help="The CORS origin(s) to allow requests from - allows all origins by default", type=click.STRING, required=False, default="*")
@click.option('--allow-origins', help="The CORS origin(s) string to allow requests from - used as the value for the 'Access-Control-Allow-Origin' response header; not configured by default", type=click.STRING)
@click.option('--dry-run', '-x', help="Execute normally without actually starting Porter", is_flag=True)
@click.option('--eager', help="Start learning and scraping the network before starting up other services", is_flag=True, default=True)
def run(general_config,
@ -81,14 +85,14 @@ def run(general_config,
# HTTP/HTTPS
if bool(tls_key_filepath) ^ bool(tls_certificate_filepath):
raise click.BadOptionUsage(option_name='--tls-key-filepath, --tls-certificate-filepath',
message=BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED)
message=PORTER_BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED)
is_https = (tls_key_filepath and tls_certificate_filepath)
# check authentication
if basic_auth_filepath and not is_https:
raise click.BadOptionUsage(option_name='--basic-auth-filepath',
message=BASIC_AUTH_REQUIRES_HTTPS)
message=PORTER_BASIC_AUTH_REQUIRES_HTTPS)
if federated_only:
if not teacher_uri:
@ -138,12 +142,17 @@ def run(general_config,
if not federated_only:
emitter.message(f"Provider: {provider_uri}", color='green')
# firm up falsy status (i.e. change specified empty string to None)
allow_origins = allow_origins if allow_origins else None
if allow_origins:
emitter.message(PORTER_CORS_ALLOWED_ORIGINS.format(allow_origins=allow_origins), color='green')
if basic_auth_filepath:
emitter.message("Basic Authentication enabled", color='green')
emitter.message(PORTER_BASIC_AUTH_ENABLED, color='green')
controller = PORTER.make_web_controller(crash_on_error=False,
htpasswd_filepath=basic_auth_filepath,
cors_origins=allow_origins)
cors_allow_origins=allow_origins)
http_scheme = "https" if is_https else "http"
message = PORTER_RUN_MESSAGE.format(http_scheme=http_scheme, http_port=http_port)
emitter.message(message, color='green', bold=True)

View File

@ -722,6 +722,10 @@ SUCCESSFUL_MANUALLY_SAVE_METADATA = "Successfully saved node metadata to {metada
PORTER_RUN_MESSAGE = "Running Porter Web Controller at {http_scheme}://127.0.0.1:{http_port}"
BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED = "Both --tls-key-filepath and --tls-certificate-filepath must be provided to launch porter with TLS; only one specified"
PORTER_BASIC_AUTH_ENABLED = "Basic Authentication enabled"
BASIC_AUTH_REQUIRES_HTTPS = "Basic authentication can only be used with HTTPS. --tls-key-filepath and --tls-certificate-filepath must also be provided"
PORTER_CORS_ALLOWED_ORIGINS = "CORS Allow Origins: {allow_origins}"
PORTER_BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED = "Both --tls-key-filepath and --tls-certificate-filepath must be provided to launch porter with TLS; only one specified"
PORTER_BASIC_AUTH_REQUIRES_HTTPS = "Basic authentication can only be used with HTTPS. --tls-key-filepath and --tls-certificate-filepath must also be provided"

View File

@ -200,7 +200,10 @@ the Pipe for nucypher network operations
self.controller = controller
return controller
def make_web_controller(self, crash_on_error: bool = False, htpasswd_filepath: Path = None, cors_origins: str = '*'):
def make_web_controller(self,
crash_on_error: bool = False,
htpasswd_filepath: Path = None,
cors_allow_origins: str = None):
controller = WebController(app_name=self.APP_NAME,
crash_on_error=crash_on_error,
interface=self._interface_class(porter=self))
@ -209,18 +212,25 @@ the Pipe for nucypher network operations
# Register Flask Decorator
porter_flask_control = controller.make_control_transport()
try:
from flask_cors import CORS
from flask_htpasswd import HtPasswdAuth
except ImportError:
raise ImportError('Porter installation is required - run "pip install nucypher[porter]" and try again.')
# CORS origins
if cors_allow_origins:
try:
from flask_cors import CORS
except ImportError:
raise ImportError('Porter installation is required for to specify CORS origins '
'- run "pip install nucypher[porter]" and try again.')
# CORS
porter_flask_control.config['CORS_ORIGINS'] = cors_origins
_ = CORS(app=porter_flask_control)
porter_flask_control.config['CORS_ORIGINS'] = cors_allow_origins
_ = CORS(app=porter_flask_control)
# Basic Auth
if htpasswd_filepath:
try:
from flask_htpasswd import HtPasswdAuth
except ImportError:
raise ImportError('Porter installation is required for basic authentication '
'- run "pip install nucypher[porter]" and try again.')
porter_flask_control.config['FLASK_HTPASSWD_PATH'] = str(htpasswd_filepath.absolute())
# ensure basic auth required for all endpoints
porter_flask_control.config['FLASK_AUTH_ALL'] = True

View File

@ -22,9 +22,10 @@ import pytest
from nucypher.characters.lawful import Ursula
from nucypher.cli.literature import (
PORTER_RUN_MESSAGE,
BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED,
BASIC_AUTH_REQUIRES_HTTPS
PORTER_BASIC_AUTH_ENABLED,
PORTER_BASIC_AUTH_REQUIRES_HTTPS,
PORTER_BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED,
PORTER_RUN_MESSAGE, PORTER_CORS_ALLOWED_ORIGINS
)
from nucypher.cli.main import nucypher_cli
from nucypher.config.constants import TEMPORARY_DOMAIN
@ -96,7 +97,7 @@ def test_federated_porter_cli_run_tls_filepath_and_certificate(click_runner,
'--tls-key-filepath', tempfile_path) # only tls-key provided
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code != 0 # both --tls-key-filepath and --tls-certificate-filepath must be provided for TLS
assert BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED in result.output
assert PORTER_BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED in result.output
porter_run_command = ('porter', 'run',
'--dry-run',
@ -105,7 +106,7 @@ def test_federated_porter_cli_run_tls_filepath_and_certificate(click_runner,
'--tls-certificate-filepath', tempfile_path) # only certificate provided
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code != 0 # both --tls-key-filepath and --tls-certificate-filepath must be provided for TLS
assert BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED in result.output
assert PORTER_BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED in result.output
#
# tls-key and certificate filepaths must exist
@ -156,6 +157,56 @@ def test_federated_cli_run_https(click_runner, federated_ursulas, temp_dir_path,
assert PORTER_RUN_MESSAGE.format(http_scheme="https", http_port=Porter.DEFAULT_PORT) in result.output
def test_federated_cli_run_https_with_cors_origin(click_runner,
federated_ursulas,
temp_dir_path,
federated_teacher_uri):
tls_key_path = Path(temp_dir_path) / 'key.pem'
_write_random_data(tls_key_path)
certificate_file_path = Path(temp_dir_path) / 'fullchain.pem'
_write_random_data(certificate_file_path)
allow_origins = "nucypher.com"
porter_run_command = ('porter', 'run',
'--dry-run',
'--federated-only',
'--teacher', federated_teacher_uri,
'--tls-key-filepath', tls_key_path,
'--tls-certificate-filepath', certificate_file_path,
'--allow-origins', allow_origins)
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code == 0
assert PORTER_RUN_MESSAGE.format(http_scheme="https", http_port=Porter.DEFAULT_PORT) in result.output
assert PORTER_CORS_ALLOWED_ORIGINS.format(allow_origins=allow_origins) in result.output
def test_federated_cli_run_https_with_empty_string_cors_origin(click_runner,
federated_ursulas,
temp_dir_path,
federated_teacher_uri):
tls_key_path = Path(temp_dir_path) / 'key.pem'
_write_random_data(tls_key_path)
certificate_file_path = Path(temp_dir_path) / 'fullchain.pem'
_write_random_data(certificate_file_path)
empty_string_allow_origins = ""
porter_run_command = ('porter', 'run',
'--dry-run',
'--federated-only',
'--teacher', federated_teacher_uri,
'--tls-key-filepath', tls_key_path,
'--tls-certificate-filepath', certificate_file_path,
'--allow-origins', empty_string_allow_origins)
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code == 0
assert PORTER_RUN_MESSAGE.format(http_scheme="https", http_port=Porter.DEFAULT_PORT) in result.output
# empty string translates to CORS not being enabled - empty origin string provides wild card comparison
# with just header
assert PORTER_CORS_ALLOWED_ORIGINS.format(allow_origins='') not in result.output
def test_federated_cli_run_https_basic_auth(click_runner,
federated_ursulas,
federated_teacher_uri,
@ -175,7 +226,7 @@ def test_federated_cli_run_https_basic_auth(click_runner,
'--basic-auth-filepath', basic_auth_file)
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code == 0
assert "Basic Authentication enabled" in result.output
assert PORTER_BASIC_AUTH_ENABLED in result.output
def test_blockchain_porter_cli_run_simple(click_runner,
@ -271,6 +322,37 @@ def test_blockchain_porter_cli_run_https(click_runner,
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code == 0
assert PORTER_RUN_MESSAGE.format(http_scheme="https", http_port=Porter.DEFAULT_PORT) in result.output
# no CORS configured by default; empty origin string provides wild card comparison with just header
assert PORTER_CORS_ALLOWED_ORIGINS.format(allow_origins='') not in result.output
def test_blockchain_porter_cli_run_https_with_cors_origin(click_runner,
blockchain_ursulas,
testerchain,
agency_local_registry,
temp_dir_path,
blockchain_teacher_uri):
tls_key_path = Path(temp_dir_path) / 'key.pem'
_write_random_data(tls_key_path)
certificate_file_path = Path(temp_dir_path) / 'fullchain.pem'
_write_random_data(certificate_file_path)
allow_origins = "*"
porter_run_command = ('porter', 'run',
'--dry-run',
'--network', TEMPORARY_DOMAIN,
'--provider', TEST_PROVIDER_URI,
'--registry-filepath', agency_local_registry.filepath,
'--teacher', blockchain_teacher_uri,
'--tls-key-filepath', tls_key_path,
'--tls-certificate-filepath', certificate_file_path,
'--allow-origins', allow_origins)
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code == 0
assert PORTER_RUN_MESSAGE.format(http_scheme="https", http_port=Porter.DEFAULT_PORT) in result.output
assert PORTER_CORS_ALLOWED_ORIGINS.format(allow_origins=allow_origins) in result.output
def test_blockchain_porter_cli_run_https_basic_auth(click_runner,
@ -297,7 +379,7 @@ def test_blockchain_porter_cli_run_https_basic_auth(click_runner,
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code != 0
assert BASIC_AUTH_REQUIRES_HTTPS in result.output
assert PORTER_BASIC_AUTH_REQUIRES_HTTPS in result.output
# Basic Auth
porter_run_command = ('porter', 'run',
@ -312,7 +394,7 @@ def test_blockchain_porter_cli_run_https_basic_auth(click_runner,
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code == 0
assert "Basic Authentication enabled" in result.output
assert PORTER_BASIC_AUTH_ENABLED in result.output
def _write_random_data(filepath: Path):