util.secscan.api: init read-only failover

This commit is contained in:
Jimmy Zelinskie 2017-01-23 14:36:19 -05:00
parent b4efa7e45b
commit e81926fcba
2 changed files with 53 additions and 22 deletions

View file

@ -310,6 +310,9 @@ class DefaultConfig(object):
# If specified, the endpoint to be used for all POST calls to the security scanner. # If specified, the endpoint to be used for all POST calls to the security scanner.
SECURITY_SCANNER_ENDPOINT_BATCH = None SECURITY_SCANNER_ENDPOINT_BATCH = None
# If specified, GET requests that return non-200 will be retried at the following instances.
SECURITY_SCANNER_READONLY_FAILOVER_ENDPOINTS = []
# The indexing engine version running inside the security scanner. # The indexing engine version running inside the security scanner.
SECURITY_SCANNER_ENGINE_VERSION_TARGET = 2 SECURITY_SCANNER_ENGINE_VERSION_TARGET = 2

View file

@ -9,6 +9,7 @@ from flask import url_for
from data.database import CloseForLongOperation from data.database import CloseForLongOperation
from data import model from data import model
from data.model.storage import get_storage_locations from data.model.storage import get_storage_locations
from util.failover import failover, FailoverException
from util.secscan.validator import SecurityConfigValidator from util.secscan.validator import SecurityConfigValidator
from util.security.instancekeys import InstanceKeys from util.security.instancekeys import InstanceKeys
from util.security.registry_jwt import generate_bearer_token, build_context_and_subject from util.security.registry_jwt import generate_bearer_token, build_context_and_subject
@ -19,6 +20,9 @@ TOKEN_VALIDITY_LIFETIME_S = 60 # Amount of time the security scanner has to cal
UNKNOWN_PARENT_LAYER_ERROR_MSG = 'worker: parent layer is unknown, it must be processed first' UNKNOWN_PARENT_LAYER_ERROR_MSG = 'worker: parent layer is unknown, it must be processed first'
MITM_CERT_PATH = '/conf/mitm.cert'
DEFAULT_HTTP_HEADERS = {'Connection': 'close'}
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -309,32 +313,56 @@ class SecurityScannerAPI(object):
return json_response return json_response
def _call(self, method, relative_url, params=None, body=None): def _request(self, method, endpoint, path, body, params, timeout):
""" Issues an HTTP call to the sec API at the given relative URL. """ Issues an HTTP request to the security endpoint. """
This function disconnects from the database while awaiting a response if self._config is None:
from the API server. raise Exception('Cannot call unconfigured security system')
url = _join_api_url(endpoint, self._config.get('SECURITY_SCANNER_API_VERSION', 'v1'), path)
signer_proxy_url = self._config.get('JWTPROXY_SIGNER', 'localhost:8080')
logger.debug('%sing security URL %s', method.upper(), url)
return self._client.request(method, url, json=body, params=params, timeout=timeout,
verify=MITM_CERT_PATH, headers=DEFAULT_HTTP_HEADERS,
proxies={'https': 'https://' + signer_proxy_url,
'http': 'http://' + signer_proxy_url})
def _call(self, method, path, params=None, body=None):
""" Issues an HTTP request to the security endpoint handling the logic of using an alternative
BATCH endpoint for non-GET requests and failover for GET requests.
""" """
if self._config is None: if self._config is None:
raise Exception('Cannot call unconfigured security system') raise Exception('Cannot call unconfigured security system')
client = self._client timeout = self._config['SECURITY_SCANNER_API_TIMEOUT_SECONDS']
headers = {'Connection': 'close'}
timeout = self._config.get('SECURITY_SCANNER_API_TIMEOUT_SECONDS', 10)
endpoint = self._config['SECURITY_SCANNER_ENDPOINT'] endpoint = self._config['SECURITY_SCANNER_ENDPOINT']
if method != 'GET':
timeout = self._config.get('SECURITY_SCANNER_API_BATCH_TIMEOUT_SECONDS', timeout)
endpoint = self._config.get('SECURITY_SCANNER_ENDPOINT_BATCH') or endpoint
api_url = urljoin(endpoint, '/' + self._config.get('SECURITY_SCANNER_API_VERSION', 'v1')) + '/'
url = urljoin(api_url, relative_url)
signer_proxy_url = self._config.get('JWTPROXY_SIGNER', 'localhost:8080')
with CloseForLongOperation(self._config): with CloseForLongOperation(self._config):
logger.debug('%sing security URL %s', method.upper(), url) # If the request isn't a read, attempt to use a batch stack and do not fail over.
return client.request(method, url, json=body, params=params, timeout=timeout, if method != 'GET':
verify='/conf/mitm.cert', headers=headers, if self._config.get('SECURITY_SCANNER_ENDPOINT_BATCH') is not None:
proxies={ endpoint = self._config['SECURITY_SCANNER_ENDPOINT_BATCH']
'https': 'https://' + signer_proxy_url, timeout = self._config.get('SECURITY_SCANNER_API_BATCH_TIMEOUT_SECONDS') or timeout
'http': 'http://' + signer_proxy_url return self._request(method, endpoint, path, body, params, timeout)
})
# The request is read-only and can failover.
all_endpoints = [endpoint] + self._config['SECURITY_SCANNER_READONLY_FAILOVER_ENDPOINTS']
try:
return _failover_read_request(*[((self._request, endpoint, path, body, params, timeout), {})
for endpoint in all_endpoints])
except FailoverException:
raise APIRequestFailure()
def _join_api_url(endpoint, api_version, path):
pathless_url = urljoin(endpoint, '/' + api_version) + '/'
return urljoin(pathless_url, path)
@failover
def _failover_read_request(request_fn, endpoint, path, body, params, timeout):
""" This function auto-retries read-only requests until they return a 2xx status code. """
resp = request_fn('GET', endpoint, path, body, params, timeout)
if resp.status_code / 100 != 2:
raise FailoverException('status code was not 2xx')
return resp