enable/disable identity modules via settings
[pithos] / pithos / im / views.py
1 # Copyright 2011 GRNET S.A. All rights reserved.
2
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
33
34 import json
35 import logging
36 import socket
37
38 from datetime import datetime
39 from functools import wraps
40 from math import ceil
41 from random import randint
42 from smtplib import SMTPException
43
44 from django.conf import settings
45 from django.core.mail import send_mail
46 from django.http import HttpResponse, HttpResponseRedirect
47 from django.shortcuts import redirect
48 from django.template.loader import render_to_string
49 from django.utils.http import urlencode
50 from django.utils.translation import ugettext as _
51 from django.core.urlresolvers import reverse
52
53 from urllib import quote
54
55 from pithos.im.models import User, Invitation
56 from pithos.im.util import isoformat
57
58
59 def render_response(template, tab=None, status=200, **kwargs):
60     if tab is None:
61         tab = template.partition('_')[0]
62     kwargs.setdefault('tab', tab)
63     html = render_to_string(template, kwargs)
64     return HttpResponse(html, status=status)
65
66
67 def requires_login(func):
68     @wraps(func)
69     def wrapper(request, *args):
70         if not settings.BYPASS_ADMIN_AUTH:
71             if not request.user:
72                 next = urlencode({'next': request.build_absolute_uri()})
73                 login_uri = reverse(index) + '?' + next
74                 return HttpResponseRedirect(login_uri)
75         return func(request, *args)
76     return wrapper
77
78
79 def requires_admin(func):
80     @wraps(func)
81     def wrapper(request, *args):
82         if not settings.BYPASS_ADMIN_AUTH:
83             if not request.user:
84                 next = urlencode({'next': request.build_absolute_uri()})
85                 login_uri = reverse(index) + '?' + next
86                 return HttpResponseRedirect(login_uri)
87             if not request.user.is_admin:
88                 return HttpResponse('Forbidden', status=403)
89         return func(request, *args)
90     return wrapper
91
92
93 def index(request):
94     kwargs = {'standard_modules':settings.IM_STANDARD_MODULES,
95               'other_modules':settings.IM_OTHER_MODULES}
96     return render_response('index.html',
97                            next=request.GET.get('next', ''),
98                            **kwargs)
99
100
101 @requires_admin
102 def admin(request):
103     stats = {}
104     stats['users'] = User.objects.count()
105     
106     invitations = Invitation.objects.all()
107     stats['invitations'] = invitations.count()
108     stats['invitations_accepted'] = invitations.filter(is_accepted=True).count()
109     
110     return render_response('admin.html', tab='home', stats=stats)
111
112
113 @requires_admin
114 def users_list(request):
115     users = User.objects.order_by('id')
116     
117     filter = request.GET.get('filter', '')
118     if filter:
119         if filter.startswith('-'):
120             users = users.exclude(uniq__icontains=filter[1:])
121         else:
122             users = users.filter(uniq__icontains=filter)
123     
124     try:
125         page = int(request.GET.get('page', 1))
126     except ValueError:
127         page = 1
128     offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT
129     limit = offset + settings.ADMIN_PAGE_LIMIT
130     
131     npages = int(ceil(1.0 * users.count() / settings.ADMIN_PAGE_LIMIT))
132     prev = page - 1 if page > 1 else None
133     next = page + 1 if page < npages else None
134     return render_response('users_list.html',
135                             users=users[offset:limit],
136                             filter=filter,
137                             pages=range(1, npages + 1),
138                             page=page,
139                             prev=prev,
140                             next=next)
141     
142 @requires_admin
143 def users_create(request):
144     if request.method == 'GET':
145         return render_response('users_create.html')
146     if request.method == 'POST':
147         user = User()
148         user.uniq = request.POST.get('uniq')
149         user.realname = request.POST.get('realname')
150         user.is_admin = True if request.POST.get('admin') else False
151         user.affiliation = request.POST.get('affiliation')
152         user.quota = int(request.POST.get('quota') or 0) * (1024 ** 3)  # In GiB
153         user.renew_token()
154         user.save()
155         return redirect(users_info, user.id)
156
157 @requires_admin
158 def users_info(request, user_id):
159     user = User.objects.get(id=user_id)
160     states = [x[0] for x in User.ACCOUNT_STATE]
161     return render_response('users_info.html',
162                             user=user,
163                             states=states)
164
165
166 @requires_admin
167 def users_modify(request, user_id):
168     user = User.objects.get(id=user_id)
169     user.uniq = request.POST.get('uniq')
170     user.realname = request.POST.get('realname')
171     user.is_admin = True if request.POST.get('admin') else False
172     user.affiliation = request.POST.get('affiliation')
173     user.state = request.POST.get('state')
174     user.invitations = int(request.POST.get('invitations') or 0)
175     user.quota = int(request.POST.get('quota') or 0) * (1024 ** 3)  # In GiB
176     user.auth_token = request.POST.get('auth_token')
177     try:
178         auth_token_expires = request.POST.get('auth_token_expires')
179         d = datetime.strptime(auth_token_expires, '%Y-%m-%dT%H:%MZ')
180         user.auth_token_expires = d
181     except ValueError:
182         pass
183     user.save()
184     return redirect(users_info, user.id)
185
186
187 @requires_admin
188 def users_delete(request, user_id):
189     user = User.objects.get(id=user_id)
190     user.delete()
191     return redirect(users_list)
192
193
194 def generate_invitation_code():
195     while True:
196         code = randint(1, 2L**63 - 1)
197         try:
198             Invitation.objects.get(code=code)
199             # An invitation with this code already exists, try again
200         except Invitation.DoesNotExist:
201             return code
202
203
204 def send_invitation(baseurl, inv):
205     url = settings.INVITATION_LOGIN_TARGET % (baseurl, inv.code, quote(baseurl))
206     subject = _('Invitation to Pithos')
207     message = render_to_string('invitation.txt', {
208                 'invitation': inv,
209                 'url': url,
210                 'baseurl': baseurl,
211                 'service': settings.SERVICE_NAME,
212                 'support': settings.DEFAULT_CONTACT_EMAIL})
213     sender = settings.DEFAULT_FROM_EMAIL
214     send_mail(subject, message, sender, [inv.uniq])
215     logging.info('Sent invitation %s', inv)
216
217
218 @requires_login
219 def invite(request):
220     status = None
221     message = None
222     inviter = request.user
223
224     if request.method == 'POST':
225         uniq = request.POST.get('uniq')
226         realname = request.POST.get('realname')
227         
228         if inviter.invitations > 0:
229             code = generate_invitation_code()
230             invitation, created = Invitation.objects.get_or_create(
231                 inviter=inviter,
232                 uniq=uniq,
233                 defaults={'code': code, 'realname': realname})
234             
235             try:
236                 send_invitation(request.get_host(), invitation)
237                 if created:
238                     inviter.invitations = max(0, inviter.invitations - 1)
239                     inviter.save()
240                 status = 'success'
241                 message = _('Invitation sent to %s' % uniq)
242             except (SMTPException, socket.error) as e:
243                 status = 'error'
244                 message = getattr(e, 'strerror', '')
245         else:
246             status = 'error'
247             message = _('No invitations left')
248
249     if request.GET.get('format') == 'json':
250         sent = [{'email': inv.uniq,
251                  'realname': inv.realname,
252                  'is_accepted': inv.is_accepted}
253                     for inv in inviter.invitations_sent.all()]
254         rep = {'invitations': inviter.invitations, 'sent': sent}
255         return HttpResponse(json.dumps(rep))
256     
257     html = render_to_string('invitations.html', {
258             'user': inviter,
259             'status': status,
260             'message': message})
261     return HttpResponse(html)
262
263 def send_verification(baseurl, user):
264     next = quote('http://%s' % baseurl)
265     url = settings.ACTIVATION_LOGIN_TARGET % (baseurl,
266                                               quote(user.auth_token),
267                                               next)
268     message = render_to_string('activation.txt', {
269             'user': user,
270             'url': url,
271             'baseurl': baseurl,
272             'service': settings.SERVICE_NAME,
273             'support': settings.DEFAULT_CONTACT_EMAIL})
274     sender = settings.DEFAULT_FROM_EMAIL
275     send_mail('Pithos account activation', message, sender, [user.email])
276     logging.info('Sent activation %s', user)
277
278 def local_create(request):
279     if request.method == 'GET':
280         return render_response('local_create.html')
281     elif request.method == 'POST':
282         username = request.POST.get('uniq')
283         realname = request.POST.get('realname')
284         email = request.POST.get('email')
285         password = request.POST.get('password')
286         status = 'success'
287         cookie_value = None
288         if not username:
289             status = 'error'
290             message = 'No username provided'
291         elif not password:
292             status = 'error'
293             message = 'No password provided'
294         elif not email:
295             status = 'error'
296             message = 'No email provided'
297         
298         if status == 'success':
299             username = '%s@local' % username
300             try:
301                 user = User.objects.get(uniq=username)
302                 status = 'error'
303                 message = 'Username is not available'
304             except User.DoesNotExist:
305                 user = User()
306                 user.uniq = username 
307                 user.realname = realname
308                 user.email = request.POST.get('email')
309                 user.password = request.POST.get('password')
310                 user.is_admin = False
311                 user.quota = 0
312                 user.state = 'UNVERIFIED'
313                 user.level = 1
314                 user.renew_token()
315                 try:
316                     send_verification(request.get_host(), user)
317                     message = _('Verification sent to %s' % user.email)
318                     user.save()
319                 except (SMTPException, socket.error) as e:
320                     status = 'error'
321                     name = 'strerror'
322                     message = getattr(e, name) if hasattr(e, name) else e
323         
324         html = render_to_string('local_create.html', {
325                 'status': status,
326                 'message': message})
327         response = HttpResponse(html)
328         return response
329
330 def send_password(baseurl, user):
331     next = quote('http://%s' % baseurl)
332     url = settings.PASSWORD_RESET_TARGET % (baseurl,
333                                             quote(user.uniq),
334                                             next)
335     message = render_to_string('password.txt', {
336             'user': user,
337             'url': url,
338             'baseurl': baseurl,
339             'service': settings.SERVICE_NAME,
340             'support': settings.DEFAULT_CONTACT_EMAIL})
341     sender = settings.DEFAULT_FROM_EMAIL
342     send_mail('Pithos password recovering', message, sender, [user.email])
343     logging.info('Sent password %s', user)
344
345 def reclaim_password(request):
346     if request.method == 'GET':
347         return render_response('reclaim.html')
348     elif request.method == 'POST':
349         username = request.POST.get('uniq')
350         username = '%s@local' % username
351         try:
352             user = User.objects.get(uniq=username)
353             try:
354                 send_password(request.get_host(), user)
355                 status = 'success'
356                 message = _('Password reset sent to %s' % user.email)
357                 user.status = 'UNVERIFIED'
358                 user.save()
359             except (SMTPException, socket.error) as e:
360                 status = 'error'
361                 name = 'strerror'
362                 message = getattr(e, name) if hasattr(e, name) else e
363         except User.DoesNotExist:
364             status = 'error'
365             message = 'Username does not exist'
366         
367         html = render_to_string('reclaim.html', {
368                 'status': status,
369                 'message': message})
370         return HttpResponse(html)