Statistics
| Branch: | Tag: | Revision:

root / invitations / invitations.py @ 7970aa87

History | View | Annotate | Download (12.8 kB)

1
# vim: set fileencoding=utf-8 :
2
# Copyright 2011 GRNET S.A. All rights reserved.
3
#
4
# Redistribution and use in source and binary forms, with or without
5
# modification, are permitted provided that the following conditions
6
# are met:
7
#
8
#   1. Redistributions of source code must retain the above copyright
9
#      notice, this list of conditions and the following disclaimer.
10
#
11
#  2. Redistributions in binary form must reproduce the above copyright
12
#     notice, this list of conditions and the following disclaimer in the
13
#     documentation and/or other materials provided with the distribution.
14
#
15
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
16
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
19
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25
# SUCH DAMAGE.
26
#
27
# The views and conclusions contained in the software and documentation are
28
# those of the authors and should not be interpreted as representing official
29
# policies, either expressed or implied, of GRNET S.A.
30

    
31

    
32
from datetime import timedelta
33
import datetime
34
import base64
35
import urllib
36
import re
37

    
38
from django.conf import settings
39
from django.core.exceptions import ValidationError
40
from django.db import transaction
41
from django.http import HttpResponse, HttpResponseRedirect, \
42
    HttpResponseBadRequest, HttpResponseServerError
43
from django.template.context import RequestContext
44
from django.template.loader import render_to_string
45
from django.core.validators import validate_email
46
from django.views.decorators.csrf import csrf_protect
47
from django.utils.translation import ugettext as _
48

    
49
from synnefo.logic.email_send import send_async, send
50
from synnefo.api.common import method_not_allowed
51
from synnefo.db.models import Invitations, SynnefoUser
52
from synnefo.logic import users
53
from synnefo.util.log import getLogger
54

    
55
from Crypto.Cipher import AES
56

    
57
log = getLogger('synnefo.invitations')
58

    
59
# override project MEDIA_URL
60
MEDIA_URL = getattr(settings, 'INVITATIONS_MEDIA_URL', '/static/invitations/')
61

    
62
def process_form(request):
63
    errors = []
64
    valid_inv = filter(lambda x: x.startswith("name_"), request.POST.keys())
65
    invitation = None
66

    
67
    for inv in valid_inv:
68
        (name, inv_id) = inv.split('_')
69

    
70
        email = ""
71
        name = ""
72
        try:
73
            email = request.POST['email_' + inv_id]
74
            name = request.POST[inv]
75

    
76
            validate_name(name)
77
            validate_email(email)
78

    
79
            invitation = add_invitation(request.user, name, email)
80
            send_invitation(invitation)
81

    
82
        except (InvitationException, ValidationError) as e:
83
            errors += ["Invitation to %s <%s> not sent. Reason: %s" %
84
                       (name, email, e.messages[0])]
85
        except Exception as e:
86
            remove_invitation(invitation)
87
            log.exception(e)
88
            errors += ["Invitation to %s <%s> could not be sent. An unexpected"
89
                       " error occurred. Please try again later." %
90
                       (name, email)]
91

    
92
    response = None
93
    if errors:
94
        data = render_to_string('invitations.html',
95
                                {'MEDIA_URL': MEDIA_URL,
96
                                 'invitations':
97
                                     invitations_for_user(request),
98
                                 'errors':
99
                                     errors,
100
                                 'invitations_left':
101
                                     get_invitations_left(request.user)},
102
                                context_instance=RequestContext(request))
103
        response = HttpResponse(data)
104
        log.warn("Error adding invitation %s -> %s: %s",
105
                    request.user.uniq, email, errors)
106
    else:
107
        # form submitted
108
        data = render_to_string('invitations.html',
109
                                {'MEDIA_URL': MEDIA_URL,
110
                                 'invitations':
111
                                    invitations_for_user(request),
112
                                 'invitations_left':
113
                                    get_invitations_left(request.user)},
114
                                context_instance=RequestContext(request))
115
        response = HttpResponse(data)
116
        log.info("Added invitation %s -> %s", request.user.uniq, email)
117

    
118
    return response
119

    
120

    
121
def validate_name(name):
122
    if name is None or name.strip() == '':
123
        raise ValidationError("Name is empty")
124

    
125
    if name.find(' ') is -1:
126
        raise ValidationError(_("Name must contain at least one space"))
127

    
128
    return True
129

    
130

    
131
def invitations_for_user(request):
132
    invitations = []
133

    
134
    for inv in Invitations.objects.filter(source=request.user).order_by("-id"):
135
        invitation = {}
136

    
137
        invitation['sourcename'] = inv.source.realname
138
        invitation['source'] = inv.source.uniq
139
        invitation['targetname'] = inv.target.realname
140
        invitation['target'] = inv.target.uniq
141
        invitation['accepted'] = inv.accepted
142
        invitation['sent'] = inv.created
143
        invitation['id'] = inv.id
144

    
145
        invitations.append(invitation)
146

    
147
    return invitations
148

    
149

    
150
@csrf_protect
151
def inv_demux(request):
152

    
153
    if request.method == 'GET':
154
        data = render_to_string('invitations.html',
155
                                {'MEDIA_URL': MEDIA_URL,
156
                                 'invitations':
157
                                     invitations_for_user(request),
158
                                 'invitations_left':
159
                                     get_invitations_left(request.user)},
160
                                context_instance=RequestContext(request))
161
        return HttpResponse(data)
162
    elif request.method == 'POST':
163
        return process_form(request)
164
    else:
165
        method_not_allowed(request)
166

    
167

    
168
def login(request):
169

    
170
    if not request.method == 'GET':
171
        method_not_allowed(request)
172

    
173
    key = request.GET.get('key', None)
174

    
175
    if key is None:
176
        return render_login_error("10", "Required key is missing")
177

    
178
    PADDING = '{'
179

    
180
    try:
181
        DecodeAES = lambda c, e: c.decrypt(base64.b64decode(e)).rstrip(PADDING)
182
        cipher = AES.new(settings.INVITATION_ENCR_KEY)
183
        decoded = DecodeAES(cipher, key)
184
    except Exception:
185
        return render_login_error("20", "Required key is invalid")
186

    
187
    users = SynnefoUser.objects.filter(auth_token=decoded)
188

    
189
    if users.count() is 0:
190
        return render_login_error("20", "Required key is invalid")
191

    
192
    user = users[0]
193
    invitations = Invitations.objects.filter(target=user)
194

    
195
    if invitations.count() is 0:
196
        return render_login_error("30", "Non-existent invitation")
197

    
198
    inv = invitations[0]
199

    
200
    valid = timedelta(days=settings.INVITATION_VALID_DAYS)
201
    valid_until = inv.created + valid
202
    now = datetime.datetime.now()
203

    
204
    if now > valid_until:
205
        return render_login_error("40",
206
                                  "Invitation has expired (was valid until " \
207
                                  "%s, now is %s" %
208
                                  (valid_until.strftime('%A, %d %B %Y'),
209
                                   now.strftime('%A, %d %B %Y')))
210

    
211
    # Since the invitation is valid, renew the user's auth token. This also
212
    # takes care of cases where the user re-uses the invitation to
213
    # login when the original token has expired
214
    from synnefo.logic import users   # redefine 'users'
215
    users.set_auth_token_expires(user, valid_until)
216

    
217
    #if inv.accepted == False:
218
    #    return render_login_error("60", "Invitation already accepted")
219

    
220
    inv.accepted = True
221
    inv.save()
222

    
223
    log.info("Invited user %s logged in", inv.target.uniq)
224

    
225
    data = dict()
226
    data['user'] = user.realname
227
    data['url'] = settings.APP_INSTALL_URL
228

    
229
    welcome = render_to_string('welcome.html', {'data': data})
230

    
231
    response = HttpResponse(welcome)
232

    
233
    response.set_cookie('X-Auth-Token',
234
                        value=user.auth_token,
235
                        expires=valid_until.strftime('%a, %d-%b-%Y %H:%M:%S %Z'),
236
                        path='/')
237
    response['X-Auth-Token'] = user.auth_token
238
    return response
239

    
240

    
241
def render_login_error(code, text):
242
    error = dict()
243
    error['id'] = code
244
    error['text'] = text
245

    
246
    data = render_to_string('error.html', {'error': error})
247

    
248
    response = HttpResponse(data)
249
    return response
250

    
251

    
252
def send_invitation(invitation):
253
    email = {}
254
    email['invitee'] = invitation.target.realname
255
    email['inviter'] = invitation.source.realname
256

    
257
    valid = timedelta(days=settings.INVITATION_VALID_DAYS)
258
    valid_until = invitation.created + valid
259
    email['valid_until'] = valid_until.strftime('%A, %d %B %Y')
260
    email['url'] = enconde_inv_url(invitation)
261

    
262
    data = render_to_string('invitation.txt', {'email': email})
263

    
264
    log.debug("Invitation URL: %s", email['url'])
265

    
266
    # send_async(
267
    #    frm = "%s"%(settings.DEFAULT_FROM_EMAIL),
268
    #    to = "%s <%s>"%(invitation.target.realname,invitation.target.uniq),
269
    #    subject = _('Invitation to ~okeanos IaaS service'),
270
    #    body = data
271
    #)
272
    send(recipient="%s <%s>" % (invitation.target.realname,
273
                                invitation.target.uniq),
274
         subject=_('Invitation to ~okeanos IaaS service'),
275
         body=data)
276

    
277

    
278
def enconde_inv_url(invitation):
279
    PADDING = '{'
280
    pad = lambda s: s + (32 - len(s) % 32) * PADDING
281
    EncodeAES = lambda c, s: base64.b64encode(c.encrypt(pad(s)))
282

    
283
    cipher = AES.new(settings.INVITATION_ENCR_KEY)
284
    encoded = EncodeAES(cipher, invitation.target.auth_token)
285

    
286
    url_safe = urllib.urlencode({'key': encoded})
287
    url = settings.APP_INSTALL_URL + "/invitations/login?" + url_safe
288

    
289
    return url
290

    
291

    
292
def resend(request):
293
    """
294
    Resend an invitation that has been already sent
295
    """
296

    
297
    if not request.method == 'POST':
298
        return method_not_allowed(request)
299

    
300
    invid = request.POST["invid"]
301

    
302
    matcher = re.compile('^[0-9]+$')
303

    
304
    # XXX: Assumes numeric DB keys
305
    if not matcher.match(invid):
306
        return HttpResponseBadRequest("Invalid content for parameter [invid]")
307

    
308
    try:
309
        inv = Invitations.objects.get(id=invid)
310
    except Exception:
311
        return HttpResponseBadRequest("Invitation to resend does not exist")
312

    
313
    if not request.user == inv.source:
314
        return HttpResponseBadRequest("Invitation does not belong to user")
315

    
316
    try:
317
        send_invitation(inv)
318
    except Exception as e:
319
        log.exception(e)
320
        return HttpResponseServerError("Error sending invitation email")
321

    
322
    return HttpResponse("Invitation has been resent")
323

    
324

    
325
def get_invitee_level(source):
326
    return get_user_inv_level(source) + 1
327

    
328

    
329
def get_user_inv_level(u):
330
    inv = Invitations.objects.filter(target=u)
331

    
332
    if not inv:
333
        raise Exception("User without invitation", u)
334

    
335
    return inv[0].level
336

    
337

    
338
@transaction.commit_on_success
339
def add_invitation(source, name, email):
340
    """
341
        Adds an invitation, if the source user has not gone over his/her
342
        invitation limit or the target user has not been invited already
343
    """
344
    num_inv = Invitations.objects.filter(source=source).count()
345

    
346
    if num_inv >= source.max_invitations:
347
        raise TooManyInvitations("User invitation limit (%d) exhausted" %
348
                                 source.max_invitations)
349

    
350
    target = SynnefoUser.objects.filter(uniq=email)
351

    
352
    if target.count() is not 0:
353
        raise AlreadyInvited("User with email %s already invited" % (email))
354

    
355
    users.register_user(name, email)
356

    
357
    target = SynnefoUser.objects.filter(uniq=email)
358

    
359
    r = list(target[:1])
360
    if not r:
361
        raise Exception("Invited user cannot be added")
362

    
363
    u = target[0]
364
    invitee_level = get_invitee_level(source)
365

    
366
    u.max_invitations = settings.INVITATIONS_PER_LEVEL[invitee_level]
367
    u.save()
368

    
369
    inv = Invitations()
370
    inv.source = source
371
    inv.target = u
372
    inv.level = invitee_level
373
    inv.save()
374
    return inv
375

    
376

    
377
def get_invitations_left(user):
378
    """
379
    Get user invitations left
380
    """
381
    num_inv = Invitations.objects.filter(source=user).count()
382
    return user.max_invitations - num_inv
383

    
384

    
385
def remove_invitation(invitation):
386
    """
387
    Removes an invitation and the invited user
388
    """
389
    if invitation is not None:
390
        if isinstance(invitation, Invitations):
391
            if invitation.target is not None:
392
                invitation.target.delete()
393
            invitation.delete()
394

    
395

    
396
class InvitationException(Exception):
397
    def __init__(self, msg):
398
        self.messages = [msg]
399

    
400

    
401
class TooManyInvitations(InvitationException):
402
    pass
403

    
404

    
405
class AlreadyInvited(InvitationException):
406
    pass