Statistics
| Branch: | Tag: | Revision:

root / invitations / invitations.py @ 4f8e7c6d

History | View | Annotate | Download (12 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, HttpResponseBadRequest, HttpResponseServerError
42
from django.template.context import RequestContext
43
from django.template.loader import render_to_string
44
from django.core.validators import validate_email
45
from django.views.decorators.csrf import csrf_protect
46
from django.utils.translation import ugettext as _
47

    
48
from synnefo.logic.email_send import send_async
49
from synnefo.api.common import method_not_allowed
50
from synnefo.db.models import Invitations, SynnefoUser
51
from synnefo.logic import users, log
52

    
53
from Crypto.Cipher import AES
54

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

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

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

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

    
71
            validate_name(name)
72
            validate_email(email)
73

    
74
            invitation = add_invitation(request.user, name, email)
75
            send_invitation(invitation)
76

    
77
        except (InvitationException, ValidationError) as e:
78
            errors += ["Invitation to %s <%s> not sent. Reason: %s" %
79
                       (name, email, e.messages[0])]
80
        except Exception as e:
81
            remove_invitation(invitation)
82
            _logger.exception(e)
83
            errors += ["Invitation to %s <%s> could not be sent. Reason: %s" %
84
                       (name, email, e)]
85

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

    
107
    return response
108

    
109

    
110
def validate_name(name):
111
    if name is None or name.strip() == '':
112
        raise ValidationError("Name is empty")
113

    
114
    if name.find(' ') is -1:
115
        raise ValidationError(_("Name must contain at least one space"))
116

    
117
    return True
118

    
119

    
120
def invitations_for_user(request):
121
    invitations = []
122

    
123
    for inv in Invitations.objects.filter(source = request.user):
124
        invitation = {}
125

    
126
        invitation['sourcename'] = inv.source.realname
127
        invitation['source'] = inv.source.uniq
128
        invitation['targetname'] = inv.target.realname
129
        invitation['target'] = inv.target.uniq
130
        invitation['accepted'] = inv.accepted
131
        invitation['sent'] = inv.created
132
        invitation['id'] = inv.id
133

    
134
        invitations.append(invitation)
135

    
136
    return invitations
137

    
138

    
139
@csrf_protect
140
def inv_demux(request):
141

    
142
    if request.method == 'GET':
143
        data = render_to_string('invitations.html',
144
                {'invitations': invitations_for_user(request),
145
                    'invitations_left': get_invitations_left(request.user)
146
                },
147
                                context_instance=RequestContext(request))
148
        return  HttpResponse(data)
149
    elif request.method == 'POST':
150
        return process_form(request)
151
    else:
152
        method_not_allowed(request)
153

    
154

    
155
def login(request):
156

    
157
    if not request.method == 'GET':
158
        method_not_allowed(request)
159

    
160
    key = request.GET['key']
161

    
162
    if key is None:
163
        return render_login_error("10", "Required key is missing")
164

    
165
    PADDING = '{'
166

    
167
    try:
168
        DecodeAES = lambda c, e: c.decrypt(base64.b64decode(e)).rstrip(PADDING)
169
        cipher = AES.new(settings.INVITATION_ENCR_KEY)
170
        decoded = DecodeAES(cipher, key)
171
    except Exception:
172
        return render_login_error("20", "Required key is invalid")
173

    
174
    users = SynnefoUser.objects.filter(auth_token = decoded)
175

    
176
    if users.count() is 0:
177
        return render_login_error("20", "Required key is invalid")
178

    
179
    user = users[0]
180
    invitations = Invitations.objects.filter(target = user)
181

    
182
    if invitations.count() is 0:
183
        return render_login_error("30", "Non-existent invitation")
184

    
185
    inv = invitations[0]
186

    
187
    valid = timedelta(days=settings.INVITATION_VALID_DAYS)
188
    valid_until = inv.created + valid
189
    now = datetime.datetime.now()
190

    
191
    if now > valid_until:
192
        return render_login_error("40",
193
                                  "Invitation has expired (was valid until " \
194
                                  "%s, now is %s" %
195
                                  (valid_until.strftime('%A, %d %B %Y'),
196
                                   now.strftime('%A, %d %B %Y')))
197

    
198
    # Since the invitation is valid, renew the user's auth token. This also
199
    # takes care of cases where the user re-uses the invitation to
200
    # login when the original token has expired
201
    from synnefo.logic import users # redefine 'users'
202
    users.set_auth_token_expires(user, valid_until)
203

    
204
    #if inv.accepted == False:
205
    #    return render_login_error("60", "Invitation already accepted")
206

    
207
    inv.accepted = True
208
    inv.save()
209

    
210
    _logger.info("Invited user %s logged in"%(inv.target.uniq))
211

    
212
    data = dict()
213
    data['user'] = user.realname
214
    data['url'] = settings.APP_INSTALL_URL
215

    
216
    welcome = render_to_string('welcome.html', {'data': data})
217

    
218
    response = HttpResponse(welcome)
219

    
220
    response.set_cookie('X-Auth-Token', value=user.auth_token,
221
                        expires = valid_until.strftime('%a, %d-%b-%Y %H:%M:%S %Z'),
222
                        path='/')
223
    response['X-Auth-Token'] = user.auth_token
224
    return response
225

    
226

    
227
def render_login_error(code, text):
228
    error = dict()
229
    error['id'] = code
230
    error['text'] = text
231

    
232
    data = render_to_string('error.html', {'error': error})
233

    
234
    response = HttpResponse(data)
235
    return response
236

    
237

    
238
def send_invitation(invitation):
239
    email = {}
240
    email['invitee'] = invitation.target.realname
241
    email['inviter'] = invitation.source.realname
242

    
243
    valid = timedelta(days = settings.INVITATION_VALID_DAYS)
244
    valid_until = invitation.created + valid
245
    email['valid_until'] = valid_until.strftime('%A, %d %B %Y')
246
    email['url'] = enconde_inv_url(invitation)
247

    
248
    data = render_to_string('invitation.txt', {'email': email})
249

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

    
252
    send_async(
253
        frm = "%s"%(settings.DEFAULT_FROM_EMAIL),
254
        to = "%s <%s>"%(invitation.target.realname,invitation.target.uniq),
255
        subject = _('Invitation to ~okeanos IaaS service'),
256
        body = data
257
    )
258

    
259
def enconde_inv_url(invitation):
260
    PADDING = '{'
261
    pad = lambda s: s + (32 - len(s) % 32) * PADDING
262
    EncodeAES = lambda c, s: base64.b64encode(c.encrypt(pad(s)))
263

    
264
    cipher = AES.new(settings.INVITATION_ENCR_KEY)
265
    encoded = EncodeAES(cipher, invitation.target.auth_token)
266

    
267
    url_safe = urllib.urlencode({'key': encoded})
268
    url = settings.APP_INSTALL_URL + "/invitations/login?" + url_safe
269

    
270
    return url
271

    
272

    
273
def resend(request):
274
    """
275
    Resend an invitation that has been already sent
276
    """
277

    
278
    if not request.method == 'POST':
279
        return method_not_allowed(request)
280

    
281
    invid = request.POST["invid"]
282

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

    
285
    # XXX: Assumes numeric DB keys
286
    if not matcher.match(invid):
287
        return HttpResponseBadRequest("Invalid content for parameter [invid]")
288

    
289
    try:
290
        inv = Invitations.objects.get(id = invid)
291
    except Exception:
292
        return HttpResponseBadRequest("Invitation to resend does not exist")
293

    
294
    if not request.user == inv.source:
295
        return HttpResponseBadRequest("Invitation does not belong to user")
296

    
297
    try:
298
        send_invitation(inv)
299
    except Exception:
300
        return HttpResponseServerError("Error sending invitation email")
301

    
302
    return HttpResponse("Invitation has been resent")
303

    
304
def get_invitee_level(source):
305
    return get_user_inv_level(source) + 1
306

    
307

    
308
def get_user_inv_level(u):
309
    inv = Invitations.objects.filter(target = u)
310

    
311
    if not inv:
312
        raise Exception("User without invitation", u)
313

    
314
    return inv[0].level
315

    
316

    
317
@transaction.commit_on_success
318
def add_invitation(source, name, email):
319
    """
320
        Adds an invitation, if the source user has not gone over his/her
321
        invitation limit or the target user has not been invited already
322
    """
323
    num_inv = Invitations.objects.filter(source = source).count()
324

    
325
    if num_inv >= source.max_invitations:
326
        raise TooManyInvitations("User invitation limit (%d) exhausted" %
327
                                 source.max_invitations)
328

    
329
    target = SynnefoUser.objects.filter(uniq = email)
330

    
331
    if target.count() is not 0:
332
        raise AlreadyInvited("User with email %s already invited" % (email))
333

    
334
    users.register_user(name, email)
335

    
336
    target = SynnefoUser.objects.filter(uniq = email)
337

    
338
    r = list(target[:1])
339
    if not r:
340
        raise Exception("Invited user cannot be added")
341

    
342
    u = target[0]
343
    invitee_level = get_invitee_level(source)
344

    
345
    u.max_invitations = settings.INVITATIONS_PER_LEVEL[invitee_level]
346
    u.save()
347

    
348
    inv = Invitations()
349
    inv.source = source
350
    inv.target = u
351
    inv.level = invitee_level
352
    inv.save()
353
    return inv
354

    
355
def get_invitations_left(user):
356
    """
357
    Get user invitations left
358
    """
359
    num_inv = Invitations.objects.filter(source = user).count()
360
    return user.max_invitations - num_inv
361

    
362
def remove_invitation(invitation):
363
    """
364
    Removes an invitation and the invited user
365
    """
366
    if invitation is not None:
367
        if isinstance(invitation, Invitations):
368
            if invitation.target is not None:
369
                invitation.target.delete()
370
            invitation.delete()
371

    
372
class InvitationException(Exception):
373
    def __init__(self, msg):
374
        self.messages = [msg]
375

    
376
class TooManyInvitations(InvitationException):
377
    pass
378

    
379
class AlreadyInvited(InvitationException):
380
    pass