3

Let's say I've created a (hopefully) reusable app, fooapp:

urls.py

urls('^(?P<userid>\d+)/$', views.show_foo),

and fooapp's views.py:

def show_foo(request, userid):
    usr = shortcuts.get_object_or_404(User, pk=userid)
    ... display a users' foo ...
    return render_to_response(...)

Since it's a reusable app, it doesn't specify any access control (e.g. @login_required).

In the site/project urls.py, the app is included:

urls('^foo/', include('fooapp.urls')),

How/where can I specify that in this site only staff members should be granted access to see a user's foo?

How about if, in addition to staff members, users should be able to view their own foo (login_required + request.user.id == userid)?

I didn't find any obvious parameters to include..

Note: this has to do with access control, not permissions, i.e. require_staff checks User.is_staff, login_required checks if request.user is logged in, and user-viewing-their-own-page is described above. This question is in regards to how a site can specify access control for a reusable app.

thebjorn
  • 26,297
  • 11
  • 96
  • 138
  • Possible duplicate of [How do I use Django groups and permissions?](http://stackoverflow.com/questions/4778685/how-do-i-use-django-groups-and-permissions) – maazza Dec 08 '15 at 15:21
  • 3
    I guess the right way to do it is to provide configuration options like `FOO_ALLOW_USER_VIEW` or `FOO_REQUIRE_STAFF` with some default values you provide, that can be overwritten in sites config. – Maciek Dec 08 '15 at 15:24
  • @maazza no, not a duplicate of that question (or answer). I don't think they're related in any way except that access control can use permissions -- although I specifically omit the need for permissions in this question. – thebjorn Dec 08 '15 at 15:29
  • @Maciek I'm not sure I understand what you mean...? The purpose of a _reusable_ app is that it should be up to the site that uses it which access control it desires, if any. The access control could depend on features of the site that the app does not know about. – thebjorn Dec 08 '15 at 15:32
  • 1
    @thebjorn When you download Celery or some other third party app to use in your Django project you have to configure it in your `settings.py`, right? You may require users of your `fooapp` to configure it in their settings.py, where they'll tell the app what access protection they want. – Maciek Dec 08 '15 at 15:35
  • @thebjorn Here is a question regarding application settings: http://stackoverflow.com/questions/8428556/django-default-settings-convention-for-pluggable-app – Maciek Dec 08 '15 at 15:38
  • @Maciek I couldn't find any access control settings for Celery.., and I think the settings approach is exactly backwards: it requires the reusable-app author to imagine all possible access controls possible (what if I wanted managers to have access to their subordinates..?) – thebjorn Dec 08 '15 at 15:41
  • @thebjorn not all controls possible - just all the ones that they want to support. – Wayne Werner Dec 08 '15 at 15:46
  • @WayneWerner the point being that an _app_ writer doesn't know (and shouldn't know) anything about access control for a _site_. If the site wants only managers to see the foo of their subordinates, that seems entirely reasonable, yet the foo-app doesn't know anything about managers or subordinates (neither has anything to do with "foo"s). – thebjorn Dec 08 '15 at 15:49
  • 1
    Which is the point, I believe, that @Maciek is trying to make. Your view only requires that the user has the `CAN_VIEW_FOO` bit, e.g. `user.has_permissions('CAN_VIEW_FOO')`, and you give it some default in your app, but it's possible create a site setting to override that... – Wayne Werner Dec 08 '15 at 15:53
  • @WayneWerner I still don't see how a setting would help (or work). If someone provides an answer that demonstrates this approach... – thebjorn Dec 08 '15 at 15:56
  • Here's another relevant question: http://stackoverflow.com/q/2307926 – Adam Taylor Mar 07 '17 at 22:02
  • Thanks @AdamTaylor. IIUC the solutions presented in that answer are variations of what I'm doing in my answer below -- i.e. recursively iterate over the the urls in question and apply the decorator to all views found. I suppose it's nice to know that what I did was sensible.. – thebjorn Mar 08 '17 at 00:41

1 Answers1

0

Well, I've found a way that works by iterating over the patterns returned by Django's include:

from django.core.urlresolvers import RegexURLPattern, RegexURLResolver

def urlpatterns_iterator(patterns):
    """Recursively iterate through `pattern`s.
    """
    _patterns = patterns[:]  # create a copy

    while _patterns:
        cur = _patterns.pop()
        if isinstance(cur, RegexURLPattern):
            yield cur
        elif isinstance(cur, RegexURLResolver):
            _patterns += cur.url_patterns
        else:
            raise ValueError("I don't know how to handle %r." % cur)

def decorate(fn, (urlconf_module, app_name, namespace)):
    """Iterate through all the urls reachable from the call to include and
       wrap the views in `fn` (which should most likely be a decorator).

       (the second argument is the return value of Django's `include`).
    """
    # if the include has a list of patterns, ie.:  url(<regex>, include([ url(..), url(..) ]))
    # then urlconf_module doesn't have 'urlpatterns' (since it's already a list).
    patterns = getattr(urlconf_module, 'urlpatterns', urlconf_module)
    for pattern in urlpatterns_iterator(patterns):
        # the .callback property will set ._callback potentially from a string representing the path to the view.
        if pattern.callback:
            # the .callback property doesn't have a setter, so access ._callback directly
            pattern._callback = fn(pattern._callback)
    return urlconf_module, app_name, namespace

this is then used in the site/project's urls.py, so instead of:

urls('^foo/', include('fooapp.urls')),

one would do:

from django.contrib.admin.views.decorators import staff_member_required as _staff_reqd

def staff_member_required(patterns):  # make an include decorator with a familiar name
    return decorate(_staff_reqd, patterns)

...
urls('^foo/', staff_member_required(include('fooapp.urls'))),
thebjorn
  • 26,297
  • 11
  • 96
  • 138