diff --git a/geonode/api/views.py b/geonode/api/views.py index 3eeebed56c2..f1d3e46e972 100644 --- a/geonode/api/views.py +++ b/geonode/api/views.py @@ -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): @@ -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) @@ -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 @@ -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") @@ -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() @@ -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' diff --git a/geonode/base/auth.py b/geonode/base/auth.py index 25e26cabb42..affb2319d5f 100644 --- a/geonode/base/auth.py +++ b/geonode/base/auth.py @@ -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()) @@ -134,7 +151,7 @@ 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(':') @@ -142,7 +159,7 @@ def get_token_from_auth_header(auth_header): 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 diff --git a/geonode/decorators.py b/geonode/decorators.py index 50c6e204b29..b249f49a0db 100644 --- a/geonode/decorators.py +++ b/geonode/decorators.py @@ -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__) @@ -38,7 +44,6 @@ def on_ogc_backend(backend_package): Useful to decorate features/tests that only available for specific backend. """ - def decorator(func): @wraps(func) @@ -46,9 +51,7 @@ def wrapper(*args, **kwargs): on_backend = check_ogc_backend(backend_package) if on_backend: return func(*args, **kwargs) - return wrapper - return decorator @@ -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. @@ -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(" ---------------------------------------------------------- ") diff --git a/geonode/geoserver/helpers.py b/geonode/geoserver/helpers.py index da8af89c5a9..efbe6c1b66f 100755 --- a/geonode/geoserver/helpers.py +++ b/geonode/geoserver/helpers.py @@ -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, diff --git a/geonode/proxy/views.py b/geonode/proxy/views.py index f9e63e47250..9aafb14e18d 100644 --- a/geonode/proxy/views.py +++ b/geonode/proxy/views.py @@ -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 @@ -100,16 +101,16 @@ 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) @@ -117,6 +118,8 @@ def get_headers(request, url, raw_url): # 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 diff --git a/geonode/settings.py b/geonode/settings.py index ce9a35e503a..5e8a767b207 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -610,6 +610,12 @@ g+gp5fQ4nmDrSNHjakzQCX2mKMsx/GLWZzoIDd7ECV9f -----END RSA PRIVATE KEY-----""" } +# In order to protect oauth2 REST endpoints, used by GeoServer to fetch user roles and +# infos, you should set this key and configure the "geonode REST role service" +# accordingly. Keep it secret! +# WARNING: If not set, the endpoint can be accessed by users without authorization. +OAUTH2_API_KEY = os.environ.get('OAUTH2_API_KEY', None) + # 1 day expiration time by default ACCESS_TOKEN_EXPIRE_SECONDS = int(os.getenv('ACCESS_TOKEN_EXPIRE_SECONDS', '86400')) diff --git a/geonode/utils.py b/geonode/utils.py index c74a1240246..b70d17f8f62 100755 --- a/geonode/utils.py +++ b/geonode/utils.py @@ -1226,6 +1226,23 @@ def raw_sql(query, params=None, ret=True): yield dict(zip(desc, row)) +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 get_client_host(request): + hostname = None + http_host = request.META.get('HTTP_HOST') + if http_host: + hostname = http_host.split(':')[0] + return hostname + + def check_ogc_backend(backend_package): """Check that geonode use a particular OGC Backend integration @@ -1288,7 +1305,7 @@ def request(self, url, method='GET', data=None, headers={}, stream=False, timeou tb = traceback.format_exc() logger.debug(tb) pass - else: + elif user == self.username: valid_uname_pw = base64.b64encode( b"%s:%s" % (self.username, self.password)).decode("ascii") headers['Authorization'] = 'Basic {}'.format(valid_uname_pw)