Statistics
| Branch: | Tag: | Revision:

root / invitations / invitations.py @ 86f046a8

History | View | Annotate | Download (12.5 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

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

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

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

    
74
            validate_name(name)
75
            validate_email(email)
76

    
77
            invitation = add_invitation(request.user, name, email)
78
            send_invitation(invitation)
79

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

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

    
114
    return response
115

    
116

    
117
def validate_name(name):
118
    if name is None or name.strip() == '':
119
        raise ValidationError("Name is empty")
120

    
121
    if name.find(' ') is -1:
122
        raise ValidationError(_("Name must contain at least one space"))
123

    
124
    return True
125

    
126

    
127
def invitations_for_user(request):
128
    invitations = []
129

    
130
    for inv in Invitations.objects.filter(source=request.user).order_by("-id"):
131
        invitation = {}
132

    
133
        invitation['sourcename'] = inv.source.realname
134
        invitation['source'] = inv.source.uniq
135
        invitation['targetname'] = inv.target.realname
136
        invitation['target'] = inv.target.uniq
137
        invitation['accepted'] = inv.accepted
138
        invitation['sent'] = inv.created
139
        invitation['id'] = inv.id
140

    
141
        invitations.append(invitation)
142

    
143
    return invitations
144

    
145

    
146
@csrf_protect
147
def inv_demux(request):
148

    
149
    if request.method == 'GET':
150
        data = render_to_string('invitations.html',
151
                                {'invitations':
152
                                     invitations_for_user(request),
153
                                 'invitations_left':
154
                                     get_invitations_left(request.user)},
155
                                context_instance=RequestContext(request))
156
        return HttpResponse(data)
157
    elif request.method == 'POST':
158
        return process_form(request)
159
    else:
160
        method_not_allowed(request)
161

    
162

    
163
def login(request):
164

    
165
    if not request.method == 'GET':
166
        method_not_allowed(request)
167

    
168
    key = request.GET.get('key', None)
169

    
170
    if key is None:
171
        return render_login_error("10", "Required key is missing")
172

    
173
    PADDING = '{'
174

    
175
    try:
176
        DecodeAES = lambda c, e: c.decrypt(base64.b64decode(e)).rstrip(PADDING)
177
        cipher = AES.new(settings.INVITATION_ENCR_KEY)
178
        decoded = DecodeAES(cipher, key)
179
    except Exception:
180
        return render_login_error("20", "Required key is invalid")
181

    
182
    users = SynnefoUser.objects.filter(auth_token=decoded)
183

    
184
    if users.count() is 0:
185
        return render_login_error("20", "Required key is invalid")
186

    
187
    user = users[0]
188
    invitations = Invitations.objects.filter(target=user)
189

    
190
    if invitations.count() is 0:
191
        return render_login_error("30", "Non-existent invitation")
192

    
193
    inv = invitations[0]
194

    
195
    valid = timedelta(days=settings.INVITATION_VALID_DAYS)
196
    valid_until = inv.created + valid
197
    now = datetime.datetime.now()
198

    
199
    if now > valid_until:
200
        return render_login_error("40",
201
                                  "Invitation has expired (was valid until " \
202
                                  "%s, now is %s" %
203
                                  (valid_until.strftime('%A, %d %B %Y'),
204
                                   now.strftime('%A, %d %B %Y')))
205

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

    
212
    #if inv.accepted == False:
213
    #    return render_login_error("60", "Invitation already accepted")
214

    
215
    inv.accepted = True
216
    inv.save()
217

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

    
220
    data = dict()
221
    data['user'] = user.realname
222
    data['url'] = settings.APP_INSTALL_URL
223

    
224
    welcome = render_to_string('welcome.html', {'data': data})
225

    
226
    response = HttpResponse(welcome)
227

    
228
    response.set_cookie('X-Auth-Token',
229
                        value=user.auth_token,
230
                        expires=valid_until.strftime('%a, %d-%b-%Y %H:%M:%S %Z'),
231
                        path='/')
232
    response['X-Auth-Token'] = user.auth_token
233
    return response
234

    
235

    
236
def render_login_error(code, text):
237
    error = dict()
238
    error['id'] = code
239
    error['text'] = text
240

    
241
    data = render_to_string('error.html', {'error': error})
242

    
243
    response = HttpResponse(data)
244
    return response
245

    
246

    
247
def send_invitation(invitation):
248
    email = {}
249
    email['invitee'] = invitation.target.realname
250
    email['inviter'] = invitation.source.realname
251

    
252
    valid = timedelta(days=settings.INVITATION_VALID_DAYS)
253
    valid_until = invitation.created + valid
254
    email['valid_until'] = valid_until.strftime('%A, %d %B %Y')
255
    email['url'] = enconde_inv_url(invitation)
256

    
257
    data = render_to_string('invitation.txt', {'email': email})
258

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

    
261
    # send_async(
262
    #    frm = "%s"%(settings.DEFAULT_FROM_EMAIL),
263
    #    to = "%s <%s>"%(invitation.target.realname,invitation.target.uniq),
264
    #    subject = _('Invitation to ~okeanos IaaS service'),
265
    #    body = data
266
    #)
267
    send(recipient="%s <%s>" % (invitation.target.realname,
268
                                invitation.target.uniq),
269
         subject=_('Invitation to ~okeanos IaaS service'),
270
         body=data)
271

    
272

    
273
def enconde_inv_url(invitation):
274
    PADDING = '{'
275
    pad = lambda s: s + (32 - len(s) % 32) * PADDING
276
    EncodeAES = lambda c, s: base64.b64encode(c.encrypt(pad(s)))
277

    
278
    cipher = AES.new(settings.INVITATION_ENCR_KEY)
279
    encoded = EncodeAES(cipher, invitation.target.auth_token)
280

    
281
    url_safe = urllib.urlencode({'key': encoded})
282
    url = settings.APP_INSTALL_URL + "/invitations/login?" + url_safe
283

    
284
    return url
285

    
286

    
287
def resend(request):
288
    """
289
    Resend an invitation that has been already sent
290
    """
291

    
292
    if not request.method == 'POST':
293
        return method_not_allowed(request)
294

    
295
    invid = request.POST["invid"]
296

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

    
299
    # XXX: Assumes numeric DB keys
300
    if not matcher.match(invid):
301
        return HttpResponseBadRequest("Invalid content for parameter [invid]")
302

    
303
    try:
304
        inv = Invitations.objects.get(id=invid)
305
    except Exception:
306
        return HttpResponseBadRequest("Invitation to resend does not exist")
307

    
308
    if not request.user == inv.source:
309
        return HttpResponseBadRequest("Invitation does not belong to user")
310

    
311
    try:
312
        send_invitation(inv)
313
    except Exception as e:
314
        log.exception(e)
315
        return HttpResponseServerError("Error sending invitation email")
316

    
317
    return HttpResponse("Invitation has been resent")
318

    
319

    
320
def get_invitee_level(source):
321
    return get_user_inv_level(source) + 1
322

    
323

    
324
def get_user_inv_level(u):
325
    inv = Invitations.objects.filter(target=u)
326

    
327
    if not inv:
328
        raise Exception("User without invitation", u)
329

    
330
    return inv[0].level
331

    
332

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

    
341
    if num_inv >= source.max_invitations:
342
        raise TooManyInvitations("User invitation limit (%d) exhausted" %
343
                                 source.max_invitations)
344

    
345
    target = SynnefoUser.objects.filter(uniq=email)
346

    
347
    if target.count() is not 0:
348
        raise AlreadyInvited("User with email %s already invited" % (email))
349

    
350
    users.register_user(name, email)
351

    
352
    target = SynnefoUser.objects.filter(uniq=email)
353

    
354
    r = list(target[:1])
355
    if not r:
356
        raise Exception("Invited user cannot be added")
357

    
358
    u = target[0]
359
    invitee_level = get_invitee_level(source)
360

    
361
    u.max_invitations = settings.INVITATIONS_PER_LEVEL[invitee_level]
362
    u.save()
363

    
364
    inv = Invitations()
365
    inv.source = source
366
    inv.target = u
367
    inv.level = invitee_level
368
    inv.save()
369
    return inv
370

    
371

    
372
def get_invitations_left(user):
373
    """
374
    Get user invitations left
375
    """
376
    num_inv = Invitations.objects.filter(source=user).count()
377
    return user.max_invitations - num_inv
378

    
379

    
380
def remove_invitation(invitation):
381
    """
382
    Removes an invitation and the invited user
383
    """
384
    if invitation is not None:
385
        if isinstance(invitation, Invitations):
386
            if invitation.target is not None:
387
                invitation.target.delete()
388
            invitation.delete()
389

    
390

    
391
class InvitationException(Exception):
392
    def __init__(self, msg):
393
        self.messages = [msg]
394

    
395

    
396
class TooManyInvitations(InvitationException):
397
    pass
398

    
399

    
400
class AlreadyInvited(InvitationException):
401
    pass