1import types
2
3from functools import wraps
4
5import l18n
6
7from django.contrib.auth import get_user_model
8from django.core.exceptions import PermissionDenied
9from django.db.models import Q
10from django.shortcuts import redirect
11from django.urls import reverse
12from django.utils.timezone import activate as activate_tz
13from django.utils.translation import gettext as _
14from django.utils.translation import override
15
16from wagtail.admin import messages
17from wagtail.core.models import GroupPagePermission
18
19
20def users_with_page_permission(page, permission_type, include_superusers=True):
21    # Get user model
22    User = get_user_model()
23
24    # Find GroupPagePermission records of the given type that apply to this page or an ancestor
25    ancestors_and_self = list(page.get_ancestors()) + [page]
26    perm = GroupPagePermission.objects.filter(permission_type=permission_type, page__in=ancestors_and_self)
27    q = Q(groups__page_permissions__in=perm)
28
29    # Include superusers
30    if include_superusers:
31        q |= Q(is_superuser=True)
32
33    return User.objects.filter(is_active=True).filter(q).distinct()
34
35
36def permission_denied(request):
37    """Return a standard 'permission denied' response"""
38    if request.is_ajax():
39        raise PermissionDenied
40
41    from wagtail.admin import messages
42
43    messages.error(request, _('Sorry, you do not have permission to access this area.'))
44    return redirect('wagtailadmin_home')
45
46
47def user_passes_test(test):
48    """
49    Given a test function that takes a user object and returns a boolean,
50    return a view decorator that denies access to the user if the test returns false.
51    """
52    def decorator(view_func):
53        # decorator takes the view function, and returns the view wrapped in
54        # a permission check
55
56        @wraps(view_func)
57        def wrapped_view_func(request, *args, **kwargs):
58            if test(request.user):
59                # permission check succeeds; run the view function as normal
60                return view_func(request, *args, **kwargs)
61            else:
62                # permission check failed
63                return permission_denied(request)
64
65        return wrapped_view_func
66
67    return decorator
68
69
70def permission_required(permission_name):
71    """
72    Replacement for django.contrib.auth.decorators.permission_required which returns a
73    more meaningful 'permission denied' response than just redirecting to the login page.
74    (The latter doesn't work anyway because Wagtail doesn't define LOGIN_URL...)
75    """
76    def test(user):
77        return user.has_perm(permission_name)
78
79    # user_passes_test constructs a decorator function specific to the above test function
80    return user_passes_test(test)
81
82
83def any_permission_required(*perms):
84    """
85    Decorator that accepts a list of permission names, and allows the user
86    to pass if they have *any* of the permissions in the list
87    """
88    def test(user):
89        for perm in perms:
90            if user.has_perm(perm):
91                return True
92
93        return False
94
95    return user_passes_test(test)
96
97
98class PermissionPolicyChecker:
99    """
100    Provides a view decorator that enforces the given permission policy,
101    returning the wagtailadmin 'permission denied' response if permission not granted
102    """
103    def __init__(self, policy):
104        self.policy = policy
105
106    def require(self, action):
107        def test(user):
108            return self.policy.user_has_permission(user, action)
109
110        return user_passes_test(test)
111
112    def require_any(self, *actions):
113        def test(user):
114            return self.policy.user_has_any_permission(user, actions)
115
116        return user_passes_test(test)
117
118
119def user_has_any_page_permission(user):
120    """
121    Check if a user has any permission to add, edit, or otherwise manage any
122    page.
123    """
124    # Can't do nothin if you're not active.
125    if not user.is_active:
126        return False
127
128    # Superusers can do anything.
129    if user.is_superuser:
130        return True
131
132    # At least one of the users groups has a GroupPagePermission.
133    # The user can probably do something.
134    if GroupPagePermission.objects.filter(group__in=user.groups.all()).exists():
135        return True
136
137    # Specific permissions for a page type do not mean anything.
138
139    # No luck! This user can not do anything with pages.
140    return False
141
142
143def reject_request(request):
144    if request.is_ajax():
145        raise PermissionDenied
146
147    # import redirect_to_login here to avoid circular imports on model files that import
148    # wagtail.admin.auth, specifically where custom user models are involved
149    from django.contrib.auth.views import redirect_to_login as auth_redirect_to_login
150    return auth_redirect_to_login(
151        request.get_full_path(), login_url=reverse('wagtailadmin_login'))
152
153
154def require_admin_access(view_func):
155    def decorated_view(request, *args, **kwargs):
156
157        user = request.user
158
159        if user.is_anonymous:
160            return reject_request(request)
161
162        if user.has_perms(['wagtailadmin.access_admin']):
163            try:
164                preferred_language = None
165                if hasattr(user, 'wagtail_userprofile'):
166                    preferred_language = user.wagtail_userprofile.get_preferred_language()
167                    l18n.set_language(preferred_language)
168                    time_zone = user.wagtail_userprofile.get_current_time_zone()
169                    activate_tz(time_zone)
170                if preferred_language:
171                    with override(preferred_language):
172                        response = view_func(request, *args, **kwargs)
173
174                    if hasattr(response, "render"):
175                        # If the response has a render() method, Django treats it
176                        # like a TemplateResponse, so we should do the same
177                        # In this case, we need to guarantee that when the TemplateResponse
178                        # is rendered, it is done within the override context manager
179                        # or the user preferred_language will not be used
180                        # (this could be replaced with simply rendering the TemplateResponse
181                        # for simplicity but this does remove some of its middleware modification
182                        # potential)
183                        render = response.render
184
185                        def overridden_render(response):
186                            with override(preferred_language):
187                                return render()
188
189                        response.render = types.MethodType(overridden_render, response)
190                        # decorate the response render method with the override context manager
191                    return response
192                else:
193                    return view_func(request, *args, **kwargs)
194
195            except PermissionDenied:
196                if request.is_ajax():
197                    raise
198
199                return permission_denied(request)
200
201        if not request.is_ajax():
202            messages.error(request, _('You do not have permission to access the admin'))
203
204        return reject_request(request)
205
206    return decorated_view
207