Statistics
| Branch: | Tag: | Revision:

root / invitations / invitations.py @ d53bbf68

History | View | Annotate | Download (10.3 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
                                    'ajax': True,
90
                                    'invitations_left': get_invitations_left(request.user)
91
                                },
92
                                context_instance=RequestContext(request))
93
        response =  HttpResponse(data)
94
        _logger.warn("Error adding invitation %s -> %s: %s"%(request.user.uniq,
95
                                                             email, errors))
96
    else:
97
        response = HttpResponseRedirect("/invitations/")
98
        _logger.info("Added invitation %s -> %s"%(request.user.uniq, email))
99

    
100
    return response
101

    
102

    
103
def validate_name(name):
104
    if name is None or name.strip() == '':
105
        raise ValidationError("Name is empty")
106

    
107
    if name.find(' ') is -1:
108
        raise ValidationError(_("Name must contain at least one space"))
109

    
110
    return True
111

    
112

    
113
def invitations_for_user(request):
114
    invitations = []
115

    
116
    for inv in Invitations.objects.filter(source = request.user):
117
        invitation = {}
118

    
119
        invitation['sourcename'] = inv.source.realname
120
        invitation['source'] = inv.source.uniq
121
        invitation['targetname'] = inv.target.realname
122
        invitation['target'] = inv.target.uniq
123
        invitation['accepted'] = inv.accepted
124
        invitation['sent'] = inv.created
125

    
126
        invitations.append(invitation)
127

    
128
    return invitations
129

    
130

    
131
@csrf_protect
132
def inv_demux(request):
133

    
134
    if request.method == 'GET':
135
        data = render_to_string('invitations.html',
136
                {'invitations': invitations_for_user(request),
137
                    'ajax': request.is_ajax(),
138
                    'invitations_left': get_invitations_left(request.user)
139
                },
140
                                context_instance=RequestContext(request))
141
        return  HttpResponse(data)
142
    elif request.method == 'POST':
143
        return process_form(request)
144
    else:
145
        method_not_allowed(request)
146

    
147

    
148
def login(request):
149

    
150
    if not request.method == 'GET':
151
        method_not_allowed(request)
152

    
153
    key = request.GET['key']
154

    
155
    if key is None:
156
        return render_login_error("10", "Required key is missing")
157

    
158
    PADDING = '{'
159

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

    
167
    users = SynnefoUser.objects.filter(auth_token = decoded)
168

    
169
    if users.count() is 0:
170
        return render_login_error("20", "Required key is invalid")
171

    
172
    user = users[0]
173
    invitations = Invitations.objects.filter(target = user)
174

    
175
    if invitations.count() is 0:
176
        return render_login_error("30", "Non-existent invitation")
177

    
178
    inv = invitations[0]
179

    
180
    valid = timedelta(days = settings.INVITATION_VALID_DAYS)
181
    valid_until = inv.created + valid
182

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

    
192
    inv.accepted = True
193
    inv.save()
194

    
195
    _logger.info("Invited user %s logged in"%(inv.target.uniq))
196

    
197
    data = dict()
198
    data['user'] = user.realname
199
    data['url'] = settings.APP_INSTALL_URL
200

    
201
    welcome = render_to_string('welcome.html', {'data': data})
202

    
203
    response = HttpResponse(welcome)
204

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

    
211

    
212
def render_login_error(code, text):
213
    error = dict()
214
    error['id'] = code
215
    error['text'] = text
216

    
217
    data = render_to_string('error.html', {'error': error})
218

    
219
    response = HttpResponse(data)
220
    return response
221

    
222

    
223
def send_invitation(invitation):
224
    email = {}
225
    email['invitee'] = invitation.target.realname
226
    email['inviter'] = invitation.source.realname
227

    
228
    valid = timedelta(days = settings.INVITATION_VALID_DAYS)
229
    valid_until = invitation.created + valid
230
    email['valid_until'] = valid_until.strftime('%A, %d %B %Y')
231

    
232
    PADDING = '{'
233
    pad = lambda s: s + (32 - len(s) % 32) * PADDING
234
    EncodeAES = lambda c, s: base64.b64encode(c.encrypt(pad(s)))
235

    
236
    cipher = AES.new(settings.INVITATION_ENCR_KEY)
237
    encoded = EncodeAES(cipher, invitation.target.auth_token)
238

    
239
    url_safe = urllib.urlencode({'key': encoded})
240

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

    
243
    data = render_to_string('invitation.txt', {'email': email})
244

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

    
247
    send_async(
248
        frm = "%s"%(settings.DEFAULT_FROM_EMAIL),
249
        to = "%s <%s>"%(invitation.target.realname,invitation.target.uniq),
250
        subject = _('Invitation to IaaS service Okeanos'),
251
        body = data
252
    )
253

    
254
def get_invitee_level(source):
255
    return get_user_inv_level(source) + 1
256

    
257

    
258
def get_user_inv_level(u):
259
    inv = Invitations.objects.filter(target = u)
260

    
261
    if not inv:
262
        raise Exception("User without invitation", u)
263

    
264
    return inv[0].level
265

    
266

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

    
275
    if num_inv >= source.max_invitations:
276
        raise TooManyInvitations("User invitation limit (%d) exhausted" %
277
                                 source.max_invitations)
278

    
279
    target = SynnefoUser.objects.filter(uniq = email)
280

    
281
    if target.count() is not 0:
282
        raise AlreadyInvited("User with email %s already invited" % (email))
283

    
284
    users.register_user(name, email)
285

    
286
    target = SynnefoUser.objects.filter(uniq = email)
287

    
288
    r = list(target[:1])
289
    if not r:
290
        raise Exception("Invited user cannot be added")
291

    
292
    u = target[0]
293
    invitee_level = get_invitee_level(source)
294

    
295
    u.max_invitations = settings.INVITATIONS_PER_LEVEL[invitee_level]
296
    u.save()
297

    
298
    inv = Invitations()
299
    inv.source = source
300
    inv.target = u
301
    inv.level = invitee_level
302
    inv.save()
303
    return inv
304

    
305

    
306
@transaction.commit_on_success
307
def invitation_accepted(invitation):
308
    """
309
        Mark an invitation as accepted
310
    """
311
    invitation.accepted = True
312
    invitation.save()
313

    
314

    
315
def get_invitations_left(user):
316
    """
317
    Get user invitations left
318
    """
319
    num_inv = Invitations.objects.filter(source = user).count()
320
    return user.max_invitations - num_inv
321

    
322
class InvitationException(Exception):
323
    def __init__(self, msg):
324
        self.messages = [msg]
325

    
326
class TooManyInvitations(InvitationException):
327
    pass
328

    
329
class AlreadyInvited(InvitationException):
330
    pass