Skip to content

Commit

Permalink
[Fixes #4543] Enforce GeoNode REST service API security
Browse files Browse the repository at this point in the history
  • Loading branch information
afabiani committed Jun 18, 2019
1 parent ceb422e commit 283fe98
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 99 deletions.
97 changes: 12 additions & 85 deletions geonode/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,21 @@
import json

from django.utils import timezone
from oauth2_provider.models import AccessToken
from oauth2_provider.exceptions import OAuthToolkitError, FatalClientError
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
from django.contrib.auth import get_user_model
from django.http import HttpResponse
from django.contrib.auth import get_user_model
from django.views.decorators.csrf import csrf_exempt

from guardian.models import Group

from oauth2_provider.models import AccessToken
from oauth2_provider.exceptions import OAuthToolkitError, FatalClientError
from allauth.account.utils import user_field, user_email, user_username

from ..base.auth import get_token_object_from_session
from ..utils import json_response
from ..decorators import superuser_or_apiauth
from ..base.auth import (
get_token_object_from_session,
extract_headers)


def verify_access_token(request, key):
Expand All @@ -54,32 +56,6 @@ def verify_access_token(request, key):
return token


def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip


def extract_headers(request):
"""
Extracts headers from the Django request object
:param request: The current django.http.HttpRequest object
:return: a dictionary with OAuthLib needed headers
"""
headers = request.META.copy()
if "wsgi.input" in headers:
del headers["wsgi.input"]
if "wsgi.errors" in headers:
del headers["wsgi.errors"]
if "HTTP_AUTHORIZATION" in headers:
headers["Authorization"] = headers["HTTP_AUTHORIZATION"]

return headers


@csrf_exempt
def user_info(request):
headers = extract_headers(request)
Expand Down Expand Up @@ -119,28 +95,12 @@ def user_info(request):
)
response['Cache-Control'] = 'no-store'
response['Pragma'] = 'no-cache'

return response


@csrf_exempt
def verify_token(request):
"""
TODO: Check IP whitelist / blacklist
Verifies the velidity of an OAuth2 Access Token
and returns associated User's details
"""

"""
No need to check authentication (see Issue #2815)
if (not request.user.is_authenticated()):
return HttpResponse(
json.dumps({
'error': 'unauthorized_request'
}),
status=403,
content_type="application/json"
)
"""

if (request.POST and 'token' in request.POST):
token = None
Expand Down Expand Up @@ -194,19 +154,8 @@ def verify_token(request):


@csrf_exempt
@superuser_or_apiauth()
def roles(request):
"""
Check IP whitelist / blacklist
"""
if settings.AUTH_IP_WHITELIST and not get_client_ip(request) in settings.AUTH_IP_WHITELIST:
return HttpResponse(
json.dumps({
'error': 'unauthorized_request'
}),
status=403,
content_type="application/json"
)

groups = [group.name for group in Group.objects.all()]
groups.append("admin")

Expand All @@ -219,19 +168,8 @@ def roles(request):


@csrf_exempt
@superuser_or_apiauth()
def users(request):
"""
Check IP whitelist / blacklist
"""
if settings.AUTH_IP_WHITELIST and not get_client_ip(request) in settings.AUTH_IP_WHITELIST:
return HttpResponse(
json.dumps({
'error': 'unauthorized_request'
}),
status=403,
content_type="application/json"
)

user_name = request.path_info.rsplit('/', 1)[-1]
User = get_user_model()

Expand Down Expand Up @@ -264,19 +202,8 @@ def users(request):


@csrf_exempt
@superuser_or_apiauth()
def admin_role(request):
"""
Check IP whitelist / blacklist
"""
if settings.AUTH_IP_WHITELIST and not get_client_ip(request) in settings.AUTH_IP_WHITELIST:
return HttpResponse(
json.dumps({
'error': 'unauthorized_request'
}),
status=403,
content_type="application/json"
)

return HttpResponse(
json.dumps({
'adminRole': 'admin'
Expand Down
21 changes: 19 additions & 2 deletions geonode/base/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@
logger = logging.getLogger(__name__)


def extract_headers(request):
"""
Extracts headers from the Django request object
:param request: The current django.http.HttpRequest object
:return: a dictionary with OAuthLib needed headers
"""
headers = request.META.copy()
if "wsgi.input" in headers:
del headers["wsgi.input"]
if "wsgi.errors" in headers:
del headers["wsgi.errors"]
if "HTTP_AUTHORIZATION" in headers:
headers["Authorization"] = headers["HTTP_AUTHORIZATION"]

return headers


def make_token_expiration(seconds=86400):
_expire_seconds = getattr(settings, 'ACCESS_TOKEN_EXPIRE_SECONDS', seconds)
_expire_time = datetime.datetime.now(timezone.get_current_timezone())
Expand Down Expand Up @@ -134,15 +151,15 @@ def delete_old_tokens(user, client='GeoServer'):
logger.debug(tb)


def get_token_from_auth_header(auth_header):
def get_token_from_auth_header(auth_header, create_if_not_exists=False):
if 'Basic' in auth_header:
encoded_credentials = auth_header.split(' ')[1] # Removes "Basic " to isolate credentials
decoded_credentials = base64.b64decode(encoded_credentials).decode("utf-8").split(':')
username = decoded_credentials[0]
password = decoded_credentials[1]
# if the credentials are correct, then the feed_bot is not None, but is a User object.
user = authenticate(username=username, password=password)
return get_auth_token(user)
return get_auth_token(user) if not create_if_not_exists else get_or_create_token(user)
elif 'Bearer' in auth_header:
return auth_header.replace('Bearer ', '')
return None
Expand Down
103 changes: 99 additions & 4 deletions geonode/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@
#
#########################################################################

import json
import logging

from functools import wraps
from django.conf import settings
from django.http import HttpResponse
from django.utils.decorators import classonlymethod
from django.core.exceptions import PermissionDenied
from geonode.utils import check_ogc_backend

from geonode.utils import (check_ogc_backend,
get_client_ip,
get_client_host)

logger = logging.getLogger(__name__)

Expand All @@ -38,17 +44,14 @@ def on_ogc_backend(backend_package):
Useful to decorate features/tests that only available for specific
backend.
"""

def decorator(func):

@wraps(func)
def wrapper(*args, **kwargs):
on_backend = check_ogc_backend(backend_package)
if on_backend:
return func(*args, **kwargs)

return wrapper

return decorator


Expand All @@ -75,6 +78,39 @@ def as_view(current, **initkwargs):
return decorator


def view_or_apiauth(view, request, test_func, *args, **kwargs):
"""
This is a helper function used by both 'logged_in_or_basicauth' and
'has_perm_or_basicauth' that does the nitty of determining if they
are already logged in or if they have provided proper http-authorization
and returning the view if all goes well, otherwise responding with a 401.
"""
if test_func(request.user) or not settings.OAUTH2_API_KEY:
# Already logged in, just return the view.
#
return view(request, *args, **kwargs)

# They are not logged in. See if they provided login credentials
#
if 'HTTP_AUTHORIZATION' in request.META:
auth = request.META['HTTP_AUTHORIZATION'].split()
if len(auth) == 2:
# NOTE: We are only support basic authentication for now.
#
if auth[0].lower() == "apikey":
auth_api_key = auth[1]
if auth_api_key and auth_api_key == settings.OAUTH2_API_KEY:
return view(request, *args, **kwargs)

# Either they did not provide an authorization header or
# something in the authorization attempt failed. Send a 401
# back to them to ask them to authenticate.
#
response = HttpResponse()
response.status_code = 401
return response


def superuser_only(function):
"""
Limit view to superusers only.
Expand All @@ -101,6 +137,65 @@ def _inner(request, *args, **kwargs):
return _inner


def superuser_protected(function):
"""Decorator that forces a view to be accessible by SUPERUSERS only.
"""
def _inner(request, *args, **kwargs):
if not request.user.is_superuser:
return HttpResponse(
json.dumps({
'error': 'unauthorized_request'
}),
status=403,
content_type="application/json"
)
return function(request, *args, **kwargs)
return _inner


def whitelist_protected(function):
"""Decorator that forces a view to be accessible by WHITE_LISTED
IPs only.
"""
def _inner(request, *args, **kwargs):
if not settings.AUTH_IP_WHITELIST or \
(get_client_ip(request) not in settings.AUTH_IP_WHITELIST and
get_client_host(request) not in settings.AUTH_IP_WHITELIST):
return HttpResponse(
json.dumps({
'error': 'unauthorized_request'
}),
status=403,
content_type="application/json"
)
return function(request, *args, **kwargs)
return _inner


def logged_in_or_apiauth():

def view_decorator(func):
def wrapper(request, *args, **kwargs):
return view_or_apiauth(func, request,
lambda u: u.is_authenticated(),
*args, **kwargs)
return wrapper

return view_decorator


def superuser_or_apiauth():

def view_decorator(func):
def wrapper(request, *args, **kwargs):
return view_or_apiauth(func, request,
lambda u: u.is_superuser,
*args, **kwargs)
return wrapper

return view_decorator


def dump_func_name(func):
def echo_func(*func_args, **func_kwargs):
logger.info(" ---------------------------------------------------------- ")
Expand Down
7 changes: 3 additions & 4 deletions geonode/geoserver/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1260,17 +1260,16 @@ def create_geoserver_db_featurestore(
'postgis' in db['ENGINE'] else db['ENGINE']
ds.connection_parameters.update(
{'Evictor run periodicity': 300,
'Estimated extends': 'true',
'Estimated extends': 'true',
'fetch size': 100000,
'encode functions': 'false',
'Expose primary keys': 'true',
'validate connections': 'true',
'Support on the fly geometry simplification': 'true',
'Connection timeout': 300,
'Support on the fly geometry simplification': 'false',
'Connection timeout': 10,
'create database': 'false',
'Batch insert size': 30,
'preparedStatements': 'true',
'preparedStatements': 'false',
'min connections': 10,
'max connections': 100,
'Evictor tests per run': 3,
Expand Down
9 changes: 6 additions & 3 deletions geonode/proxy/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
http_client,
json_response)
from geonode.base.auth import (extend_token,
get_or_create_token,
get_token_from_auth_header,
get_token_object_from_session)
from geonode.base.enumerations import LINK_TYPES as _LT
Expand Down Expand Up @@ -100,23 +101,25 @@ def get_headers(request, url, raw_url):
headers["Content-Type"] = request.META["CONTENT_TYPE"]

access_token = None

site_url = urlsplit(settings.SITEURL)
if site_url.netloc == url.netloc:
if site_url.hostname == url.hostname:
# we give precedence to obtained from Aithorization headers
if 'HTTP_AUTHORIZATION' in request.META:
auth_header = request.META.get(
'HTTP_AUTHORIZATION',
request.META.get('HTTP_AUTHORIZATION2'))
if auth_header:
access_token = get_token_from_auth_header(auth_header)
headers['Authorization'] = auth_header
access_token = get_token_from_auth_header(auth_header, create_if_not_exists=True)
# otherwise we check if a session is active
elif request and request.user.is_authenticated:
access_token = get_token_object_from_session(request.session)

# we extend the token in case the session is active but the token expired
if access_token and access_token.is_expired():
extend_token(access_token)
else:
access_token = get_or_create_token(request.user)

if access_token:
headers['Authorization'] = 'Bearer %s' % access_token
Expand Down

0 comments on commit 283fe98

Please sign in to comment.