Statistics
| Branch: | Tag: | Revision:

root / invitations / invitations.py @ 4af67649

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, log
53

    
54
from Crypto.Cipher import AES
55

    
56
_logger = log.get_logger("synnefo.invitations")
57

    
58

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

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

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

    
73
            validate_name(name)
74
            validate_email(email)
75

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

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

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

    
113
    return response
114

    
115

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

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

    
123
    return True
124

    
125

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

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

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

    
140
        invitations.append(invitation)
141

    
142
    return invitations
143

    
144

    
145
@csrf_protect
146
def inv_demux(request):
147

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

    
161

    
162
def login(request):
163

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

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

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

    
172
    PADDING = '{'
173

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

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

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

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

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

    
192
    inv = invitations[0]
193

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

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

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

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

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

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

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

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

    
225
    response = HttpResponse(welcome)
226

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

    
234

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

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

    
242
    response = HttpResponse(data)
243
    return response
244

    
245

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

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

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

    
258
    _logger.debug("Invitation URL: %s" % email['url'])
259

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

    
271

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

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

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

    
283
    return url
284

    
285

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

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

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

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

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

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

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

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

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

    
318

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

    
322

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

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

    
329
    return inv[0].level
330

    
331

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

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

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

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

    
349
    users.register_user(name, email)
350

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

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

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

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

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

    
370

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

    
378

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

    
389

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

    
394

    
395
class TooManyInvitations(InvitationException):
396
    pass
397

    
398

    
399
class AlreadyInvited(InvitationException):
400
    pass