Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security decorator should log client IP address #415

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions connexion/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def __init__(self, specification, base_url=None, arguments=None,
swagger_json=None, swagger_ui=None, swagger_path=None, swagger_url=None,
validate_responses=False, strict_validation=False, resolver=None,
auth_all_paths=False, debug=False, resolver_error_handler=None,
validator_map=None, pythonic_params=False):
validator_map=None, pythonic_params=False, trusted_ips=None):
"""
:type specification: pathlib.Path | dict
:type base_url: str | None
Expand All @@ -85,6 +85,9 @@ def __init__(self, specification, base_url=None, arguments=None,
:param pythonic_params: When True CamelCase parameters are converted to snake_case and an underscore is appended
to any shadowed built-ins
:type pythonic_params: bool
:param trusted_ips: A list of trusted IPs. If request.remote_addr is in this list (i.e. it's a proxy we control)
then we'll also trust the X-Forwarded-For HTTP header.
:type trusted_ips: list
"""
self.debug = debug
self.validator_map = validator_map
Expand Down Expand Up @@ -149,6 +152,9 @@ def __init__(self, specification, base_url=None, arguments=None,
logger.debug('Pythonic params: %s', str(pythonic_params))
self.pythonic_params = pythonic_params

logger.debug('Trusted IPs: %s', str(trusted_ips))
self.trusted_ips = trusted_ips or []

# Create blueprint and endpoints
self.blueprint = self.create_blueprint()

Expand Down Expand Up @@ -194,7 +200,8 @@ def add_operation(self, method, path, swagger_operation, path_parameters):
validator_map=self.validator_map,
strict_validation=self.strict_validation,
resolver=self.resolver,
pythonic_params=self.pythonic_params)
pythonic_params=self.pythonic_params,
trusted_ips=self.trusted_ips)
self._add_operation_internal(method, path, operation)

def _add_resolver_error_handler(self, method, path, err):
Expand Down Expand Up @@ -274,8 +281,10 @@ def add_auth_on_not_found(self):
Adds a 404 error handler to authenticate and only expose the 404 status if the security validation pass.
"""
logger.debug('Adding path not found authentication')
not_found_error = AuthErrorHandler(werkzeug.exceptions.NotFound(), security=self.security,
security_definitions=self.security_definitions)
not_found_error = AuthErrorHandler(werkzeug.exceptions.NotFound(),
security=self.security,
security_definitions=self.security_definitions,
trusted_ips=self.trusted_ips)
endpoint_name = "{name}_not_found".format(name=self.blueprint.name)
self.blueprint.add_url_rule('/<path:invalid_path>', endpoint_name, not_found_error.function)

Expand Down
34 changes: 27 additions & 7 deletions connexion/decorators/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,27 @@ def get_tokeninfo_url(security_definition):
return token_info_url


def get_client_ip(request, trusted_ips):
if request.headers.get("X-Forwarded-For") and request.remote_addr in trusted_ips:
return request.headers.get("X-Forwarded-For")
return request.remote_addr


def nice_scopes(scopes):
if not scopes:
return "None"
return ','.join(scopes)


def security_passthrough(function):
"""
:type function: types.FunctionType
:rtype: types.FunctionType
:rtype: types.FunctionTyperequest.headers
"""
return function


def verify_oauth(token_info_url, allowed_scopes, function):
def verify_oauth(token_info_url, allowed_scopes, trusted_ips, function):
"""
Decorator to verify oauth

Expand All @@ -49,7 +61,11 @@ def verify_oauth(token_info_url, allowed_scopes, function):
:type allowed_scopes: set
:type function: types.FunctionType
:rtype: types.FunctionType
:param trusted_ips: A list of trusted IPs. If request.remote_addr is in this list (i.e. it's a proxy we control)
then we'll also trust the X-Forwarded-For HTTP header.
:type trusted_ips: list
"""
trusted_ips = trusted_ips or []

@functools.wraps(function)
def wrapper(*args, **kwargs):
Expand All @@ -65,21 +81,25 @@ def wrapper(*args, **kwargs):
raise OAuthProblem(description='Invalid authorization header')
logger.debug("... Getting token from %s", token_info_url)
token_request = session.get(token_info_url, params={'access_token': token}, timeout=5)
logger.debug("... Token info (%d): %s", token_request.status_code, token_request.text)
client_ip = get_client_ip(request, trusted_ips)
logger.debug("... Token info (%d): %s for client IP '%s'",
token_request.status_code,
token_request.text,
client_ip)
if not token_request.ok:
raise OAuthResponseProblem(
description='Provided oauth token is not valid',
token_response=token_request
)
token_info = token_request.json() # type: dict
user_scopes = set(token_info['scope'])
logger.debug("... Scopes required: %s", allowed_scopes)
logger.debug("... User scopes: %s", user_scopes)
logger.debug("... Scopes required: %s", nice_scopes(allowed_scopes))
logger.debug("... User scopes: %s", nice_scopes(user_scopes))
if not allowed_scopes <= user_scopes:
logger.info(textwrap.dedent("""
... User scopes (%s) do not match the scopes necessary to call endpoint (%s).
Aborting with 403.""").replace('\n', ''),
user_scopes, allowed_scopes)
Aborting with 403 for client IP '%s'""").replace('\n', ''),
nice_scopes(user_scopes), nice_scopes(allowed_scopes), client_ip)
raise OAuthScopeProblem(
description='Provided token doesn\'t have the required scope',
required_scopes=allowed_scopes,
Expand Down
8 changes: 6 additions & 2 deletions connexion/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class AuthErrorHandler(SecureOperation):
Wraps an error with authentication.
"""

def __init__(self, exception, security, security_definitions):
def __init__(self, exception, security, security_definitions, trusted_ips=None):
"""
This class uses the exception instance to produce the proper response problem in case the
request is authenticated.
Expand All @@ -23,9 +23,13 @@ def __init__(self, exception, security, security_definitions):
:param security_definitions: `Security Definitions Object
<https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#security-definitions-object>`_
:type security_definitions: dict
:param trusted_ips: A list of trusted IPs. If request.remote_addr is in this list (i.e. it's a proxy we control)
then we'll also trust the X-Forwarded-For HTTP header.
:type trusted_ips: list
"""
self.exception = exception
SecureOperation.__init__(self, security, security_definitions)
trusted_ips = trusted_ips or []
SecureOperation.__init__(self, security, security_definitions, trusted_ips)

@property
def function(self):
Expand Down
14 changes: 11 additions & 3 deletions connexion/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,20 @@

class SecureOperation(object):

def __init__(self, security, security_definitions):
def __init__(self, security, security_definitions, trusted_ips=None):
"""
:param security: list of security rules the application uses by default
:type security: list
:param security_definitions: `Security Definitions Object
<https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#security-definitions-object>`_
:type security_definitions: dict
:param trusted_ips: A list of trusted IPs. If request.remote_addr is in this list (i.e. it's a proxy we control)
then we'll also trust the X-Forwarded-For HTTP header.
:type trusted_ips: list
"""
self.security = security
self.security_definitions = security_definitions
self.trusted_ips = trusted_ips or []

@property
def security_decorator(self):
Expand Down Expand Up @@ -84,7 +88,7 @@ def security_decorator(self):
token_info_url = get_tokeninfo_url(security_definition)
if token_info_url:
scopes = set(scopes) # convert scopes to set because this is needed for verify_oauth
return functools.partial(verify_oauth, token_info_url, scopes)
return functools.partial(verify_oauth, token_info_url, scopes, self.trusted_ips)
else:
logger.warning("... OAuth2 token info URL missing. **IGNORING SECURITY REQUIREMENTS**",
extra=vars(self))
Expand Down Expand Up @@ -133,7 +137,7 @@ def __init__(self, method, path, operation, resolver, app_produces, app_consumes
path_parameters=None, app_security=None, security_definitions=None,
definitions=None, parameter_definitions=None, response_definitions=None,
validate_responses=False, strict_validation=False, randomize_endpoint=None,
validator_map=None, pythonic_params=False):
validator_map=None, pythonic_params=False, trusted_ips=None):
"""
This class uses the OperationID identify the module and function that will handle the operation

Expand Down Expand Up @@ -180,13 +184,17 @@ def __init__(self, method, path, operation, resolver, app_produces, app_consumes
:param pythonic_params: When True CamelCase parameters are converted to snake_case and an underscore is appended
to any shadowed built-ins
:type pythonic_params: bool
:param trusted_ips: A list of trusted IPs. If request.remote_addr is in this list (i.e. it's a proxy we control)
then we'll also trust the X-Forwarded-For HTTP header.
:type trusted_ips: list
"""

self.method = method
self.path = path
self.validator_map = dict(VALIDATOR_MAP)
self.validator_map.update(validator_map or {})
self.security_definitions = security_definitions or {}
self.trusted_ips = trusted_ips or []
self.definitions = definitions or {}
self.parameter_definitions = parameter_definitions or {}
self.response_definitions = response_definitions or {}
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def read_version(package):
'decorator',
'mock',
'pytest',
'pytest-cov'
'pytest-cov',
'testfixtures'
]


Expand Down
Loading