Source code for geonode.decorators

#########################################################################
#
# Copyright (C) 2016 OSGeo
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################

import json
import base64
import logging

from functools import wraps

from django.contrib import auth
from django.conf import settings
from django.http import HttpResponse
from django.contrib.auth import authenticate, login
from django.utils.decorators import classonlymethod
from django.core.exceptions import PermissionDenied

from geonode.utils import check_ogc_backend, get_client_ip, get_client_host

[docs] logger = logging.getLogger(__name__)
[docs] def on_ogc_backend(backend_package): """ Decorator for function specific to a certain ogc backend. This decorator will wrap function so it only gets executed if the specified ogc backend is currently used. If not, the function will just be skipped. 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
[docs] def view_or_basicauth(view, request, test_func, realm="", *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): # 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: basic_auth = request.META["HTTP_AUTHORIZATION"].split() if len(basic_auth) == 2: # NOTE: We are only support basic authentication for now. # if basic_auth[0].lower() == "basic": uname, passwd = base64.b64decode(basic_auth[1]).decode("utf-8").split(":", 1) user = authenticate(username=uname, password=passwd) if user and user.is_active: login(request, user) request.user = user auth.update_session_auth_hash(request, user) 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 response["WWW-Authenticate"] = f'Basic realm="{realm}"' return response
[docs] def view_decorator(fdec, subclass=False): """ Change a function decorator into a view decorator. https://github.com/lqc/django/tree/cbvdecoration_ticket14512 """ def decorator(cls): if not hasattr(cls, "as_view"): raise TypeError("You should only decorate subclasses of View, not mixins.") if subclass: cls = type(f"{cls.__name__}WithDecorator({fdec.__name__})", (cls,), {}) original = cls.as_view.__func__ @wraps(original) def as_view(current, **initkwargs): return fdec(original(current, **initkwargs)) cls.as_view = classonlymethod(as_view) return cls return decorator
[docs] 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(auth.get_user(request)) 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
[docs] def has_perm_or_basicauth(perm, realm=""): """ This is similar to the above decorator 'logged_in_or_basicauth' except that it requires the logged in user to have a specific permission. Use: @logged_in_or_basicauth('asforums.view_forumcollection') def your_view: ... """ def view_decorator(func): def wrapper(request, *args, **kwargs): return view_or_basicauth(func, request, lambda u: u.has_perm(perm), realm, *args, **kwargs) return wrapper return view_decorator
[docs] def superuser_only(function): """ Restricts access to the view for superusers only. **Usage:** To restrict a view to superusers, use the `@superuser_only` decorator as follows: .. code-block:: python @superuser_only def my_view(request): ... Or use it in URL patterns: .. code-block:: python urlpatterns = [ path('foobar/<str:param>', superuser_only(my_view)), ] """ def _inner(request, *args, **kwargs): if not auth.get_user(request).is_superuser and not auth.get_user(request).is_staff: raise PermissionDenied return function(request, *args, **kwargs) return _inner
[docs] def check_keyword_write_perms(function): def _inner(request, *args, **kwargs): keyword_readonly = ( settings.FREETEXT_KEYWORDS_READONLY and request.method == "POST" and not auth.get_user(request).is_superuser ) request.keyword_readonly = keyword_readonly if keyword_readonly and "resource-keywords" in request.POST: return HttpResponse( "Unauthorized: Cannot edit/create Free-text Keywords", status=401, content_type="application/json" ) return function(request, *args, **kwargs) return _inner
[docs] def superuser_protected(function): """ Decorator that forces a view to be accessible by SUPERUSERS only. """ def _inner(request, *args, **kwargs): if not auth.get_user(request).is_superuser: return HttpResponse( json.dumps({"error": "unauthorized_request"}), status=403, content_type="application/json" ) return function(request, *args, **kwargs) return _inner
[docs] 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
[docs] def logged_in_or_basicauth(realm=""): """ A simple decorator that requires a user to be logged in. If they are not logged in the request is examined for a 'authorization' header. If the header is present it is tested for basic authentication and the user is logged in with the provided credentials. If the header is not present a http 401 is sent back to the requestor to provide credentials. The purpose of this is that in several django projects I have needed several specific views that need to support basic authentication, yet the web site as a whole used django's provided authentication. The uses for this are for urls that are access programmatically such as by rss feed readers, yet the view requires a user to be logged in. Many rss readers support supplying the authentication credentials via http basic auth (and they do NOT support a redirect to a form where they post a username/password.) Use is simple: .. code-block:: python @logged_in_or_basicauth() def your_view: ... You can provide the name of the realm to ask for authentication within. """ def view_decorator(func): def wrapper(request, *args, **kwargs): return view_or_basicauth(func, request, lambda u: u.is_authenticated, realm, *args, **kwargs) return wrapper return view_decorator
[docs] 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
[docs] 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
[docs] def dump_func_name(func): def echo_func(*func_args, **func_kwargs): logger.debug(f"Start func: {func.__name__}") return func(*func_args, **func_kwargs) return echo_func