Statistics
| Branch: | Tag: | Revision:

root / invitations / invitations.py @ 26f1983a

History | View | Annotate | Download (10.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 base64
34
import time
35
import urllib
36

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

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

    
52
from Crypto.Cipher import AES
53

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

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

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

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

    
69
            validate_name(name)
70
            validate_email(email)
71

    
72
            inv = add_invitation(request.user, name, email)
73
            send_invitation(inv)
74

    
75
        # FIXME: Delete invitation and user on error
76
        except (InvitationException, ValidationError) as e:
77
            errors += ["Invitation to %s <%s> not sent. Reason: %s" %
78
                       (name, email, e.messages[0])]
79
        except Exception as e:
80
            _logger.exception(e)
81
            errors += ["Invitation to %s <%s> not sent. Reason: %s" %
82
                       (name, email, e.message)]
83

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

    
105
    return response
106

    
107

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

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

    
115
    return True
116

    
117

    
118
def invitations_for_user(request):
119
    invitations = []
120

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

    
124
        invitation['sourcename'] = inv.source.realname
125
        invitation['source'] = inv.source.uniq
126
        invitation['targetname'] = inv.target.realname
127
        invitation['target'] = inv.target.uniq
128
        invitation['accepted'] = inv.accepted
129
        invitation['sent'] = inv.created
130

    
131
        invitations.append(invitation)
132

    
133
    return invitations
134

    
135

    
136
@csrf_protect
137
def inv_demux(request):
138

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

    
151

    
152
def login(request):
153

    
154
    if not request.method == 'GET':
155
        method_not_allowed(request)
156

    
157
    key = request.GET['key']
158

    
159
    if key is None:
160
        return render_login_error("10", "Required key is missing")
161

    
162
    PADDING = '{'
163

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

    
171
    users = SynnefoUser.objects.filter(auth_token = decoded)
172

    
173
    if users.count() is 0:
174
        return render_login_error("20", "Required key is invalid")
175

    
176
    user = users[0]
177
    invitations = Invitations.objects.filter(target = user)
178

    
179
    if invitations.count() is 0:
180
        return render_login_error("30", "Non-existent invitation")
181

    
182
    inv = invitations[0]
183

    
184
    valid = timedelta(days = settings.INVITATION_VALID_DAYS)
185
    valid_until = inv.created + valid
186

    
187
    if (time.time() -
188
        time.mktime(inv.created.timetuple()) -
189
        settings.INVITATION_VALID_DAYS * 3600) > 0:
190
        return render_login_error("40",
191
                                  "Invitation expired (was valid until %s)"%
192
                                  valid_until.strftime('%A, %d %B %Y'))
193
    #if inv.accepted == False:
194
    #    return render_login_error("60", "Invitation already accepted")
195

    
196
    inv.accepted = True
197
    inv.save()
198

    
199
    _logger.info("Invited user %s logged in"%(inv.target.uniq))
200

    
201
    data = dict()
202
    data['user'] = user.realname
203
    data['url'] = settings.APP_INSTALL_URL
204

    
205
    welcome = render_to_string('welcome.html', {'data': data})
206

    
207
    response = HttpResponse(welcome)
208

    
209
    response.set_cookie('X-Auth-Token', value=user.auth_token,
210
                        expires = valid_until.strftime('%a, %d-%b-%Y %H:%M:%S %Z'),
211
                        path='/')
212
    response['X-Auth-Token'] = user.auth_token
213
    return response
214

    
215

    
216
def render_login_error(code, text):
217
    error = dict()
218
    error['id'] = code
219
    error['text'] = text
220

    
221
    data = render_to_string('error.html', {'error': error})
222

    
223
    response = HttpResponse(data)
224
    return response
225

    
226

    
227
def send_invitation(invitation):
228
    email = {}
229
    email['invitee'] = invitation.target.realname
230
    email['inviter'] = invitation.source.realname
231

    
232
    valid = timedelta(days = settings.INVITATION_VALID_DAYS)
233
    valid_until = invitation.created + valid
234
    email['valid_until'] = valid_until.strftime('%A, %d %B %Y')
235

    
236
    PADDING = '{'
237
    pad = lambda s: s + (32 - len(s) % 32) * PADDING
238
    EncodeAES = lambda c, s: base64.b64encode(c.encrypt(pad(s)))
239

    
240
    cipher = AES.new(settings.INVITATION_ENCR_KEY)
241
    encoded = EncodeAES(cipher, invitation.target.auth_token)
242

    
243
    url_safe = urllib.urlencode({'key': encoded})
244

    
245
    email['url'] = settings.APP_INSTALL_URL + "/invitations/login?" + url_safe
246

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

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

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

    
258
def get_invitee_level(source):
259
    return get_user_inv_level(source) + 1
260

    
261

    
262
def get_user_inv_level(u):
263
    inv = Invitations.objects.filter(target = u)
264

    
265
    if not inv:
266
        raise Exception("User without invitation", u)
267

    
268
    return inv[0].level
269

    
270

    
271
@transaction.commit_on_success
272
def add_invitation(source, name, email):
273
    """
274
        Adds an invitation, if the source user has not gone over his/her
275
        invitation limit or the target user has not been invited already
276
    """
277
    num_inv = Invitations.objects.filter(source = source).count()
278

    
279
    if num_inv >= source.max_invitations:
280
        raise TooManyInvitations("User invitation limit (%d) exhausted" %
281
                                 source.max_invitations)
282

    
283
    target = SynnefoUser.objects.filter(uniq = email)
284

    
285
    if target.count() is not 0:
286
        raise AlreadyInvited("User with email %s already invited" % (email))
287

    
288
    users.register_user(name, email)
289

    
290
    target = SynnefoUser.objects.filter(uniq = email)
291

    
292
    r = list(target[:1])
293
    if not r:
294
        raise Exception("Invited user cannot be added")
295

    
296
    u = target[0]
297
    invitee_level = get_invitee_level(source)
298

    
299
    u.max_invitations = settings.INVITATIONS_PER_LEVEL[invitee_level]
300
    u.save()
301

    
302
    inv = Invitations()
303
    inv.source = source
304
    inv.target = u
305
    inv.level = invitee_level
306
    inv.save()
307
    return inv
308

    
309

    
310
@transaction.commit_on_success
311
def invitation_accepted(invitation):
312
    """
313
        Mark an invitation as accepted
314
    """
315
    invitation.accepted = True
316
    invitation.save()
317

    
318

    
319
def get_invitations_left(user):
320
    """
321
    Get user invitations left
322
    """
323
    num_inv = Invitations.objects.filter(source = user).count()
324
    return user.max_invitations - num_inv
325

    
326
class InvitationException(Exception):
327
    def __init__(self, msg):
328
        self.messages = [msg]
329

    
330
class TooManyInvitations(InvitationException):
331
    pass
332

    
333
class AlreadyInvited(InvitationException):
334
    pass