fix typo
[mygpo.git] / mygpo / api / basic_auth.py
blobab5b805f85f58ccbf88944cc31527d31e69c9ad7
2 # This file is part of my.gpodder.org.
4 # my.gpodder.org is free software: you can redistribute it and/or modify it
5 # under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or (at your
7 # option) any later version.
9 # my.gpodder.org is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
12 # License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with my.gpodder.org. If not, see <http://www.gnu.org/licenses/>.
18 from functools import wraps
20 from django.http import HttpResponse, HttpResponseBadRequest
21 from django.contrib.auth import authenticate
23 from mygpo.decorators import repeat_on_conflict
26 import logging
27 logger = logging.getLogger(__name__)
30 @repeat_on_conflict(['user'])
31 def login(request, user):
32 from django.contrib.auth import login
33 login(request, user)
37 #############################################################################
39 def view_or_basicauth(view, request, test_func, realm = "", *args, **kwargs):
40 """
41 This is a helper function used by both 'require_valid_user' and
42 'has_perm_or_basicauth' that does the nitty of determining if they
43 are already logged in or if they have provided proper http-authorization
44 and returning the view if all goes well, otherwise responding with a 401.
45 """
46 if test_func(request.user):
47 # Already logged in, just return the view.
48 return view(request, *args, **kwargs)
50 # They are not logged in. See if they provided login credentials
52 # the AUTHORIZATION header is used when passing auth-headers
53 # from Aapache to fcgi
54 auth = None
55 for h in ('AUTHORIZATION', 'HTTP_AUTHORIZATION'):
56 auth = request.META.get(h, auth)
58 if not auth:
59 return auth_request()
62 auth = auth.split(None, 1)
64 if len(auth) == 2:
65 auth_type, credentials = auth
67 # NOTE: We are only support basic authentication for now.
68 if auth_type.lower() == 'basic':
69 try:
70 credentials = credentials.decode('base64').split(':', 1)
72 except UnicodeDecodeError as e:
73 return HttpResponseBadRequest(
74 'Could not decode credentials: {msg}'.format(msg=str(e)))
76 if len(credentials) == 2:
77 uname, passwd = credentials
78 user = authenticate(username=uname, password=passwd)
79 if user is not None and user.is_active:
80 login(request, user=user)
81 request.user = user
83 return view(request, *args, **kwargs)
85 return auth_request()
88 def auth_request(realm=''):
89 # Either they did not provide an authorization header or
90 # something in the authorization attempt failed. Send a 401
91 # back to them to ask them to authenticate.
92 response = HttpResponse()
93 response.status_code = 401
94 response['WWW-Authenticate'] = 'Basic realm="%s"' % realm
95 return response
98 #############################################################################
100 def require_valid_user(protected_view):
102 A simple decorator that requires a user to be logged in. If they are not
103 logged in the request is examined for a 'authorization' header.
105 If the header is present it is tested for basic authentication and
106 the user is logged in with the provided credentials.
108 If the header is not present a http 401 is sent back to the
109 requestor to provide credentials.
111 The purpose of this is that in several django projects I have needed
112 several specific views that need to support basic authentication, yet the
113 web site as a whole used django's provided authentication.
115 The uses for this are for urls that are access programmatically such as
116 by rss feed readers, yet the view requires a user to be logged in. Many rss
117 readers support supplying the authentication credentials via http basic
118 auth (and they do NOT support a redirect to a form where they post a
119 username/password.)
121 XXX: Fix usage descriptions, ideally provide an example as doctest.
123 @wraps(protected_view)
124 def wrapper(request, *args, **kwargs):
125 def check_valid_user(user):
126 return user.is_authenticated()
128 return view_or_basicauth(protected_view, \
129 request, \
130 check_valid_user, \
131 '', \
132 *args, \
133 **kwargs)
134 return wrapper
137 def check_username(protected_view):
139 decorator to check whether the username passed to the view (from the URL)
140 matches the username with which the user is authenticated.
142 @wraps(protected_view)
143 def wrapper(request, username, *args, **kwargs):
145 if request.user.username.lower() == username.lower():
146 return protected_view(request, *args, username=username, **kwargs)
148 else:
149 # TODO: raise SuspiciousOperation here?
150 logger.warn('username in authentication (%s) and in requested resource (%s) don\'t match' % (request.user.username, username))
151 return HttpResponseBadRequest('username in authentication (%s) and in requested resource (%s) don\'t match' % (request.user.username, username))
153 return wrapper
156 #############################################################################
158 def has_perm_or_basicauth(perm, realm = ""):
160 This is similar to the above decorator 'logged_in_or_basicauth'
161 except that it requires the logged in user to have a specific
162 permission.
164 Use:
166 @logged_in_or_basicauth('asforums.view_forumcollection')
167 def your_view:
171 def view_decorator(func):
172 @wraps(func)
173 def wrapper(request, *args, **kwargs):
174 return view_or_basicauth(func, request,
175 lambda u: u.has_perm(perm),
176 realm, *args, **kwargs)
177 return wrapper
178 return view_decorator