Statistics
| Branch: | Tag: | Revision:

root / invitations / invitations.py @ 06b7df28

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

    
83
    respose = None
84
    if errors:
85
        data = render_to_string('invitations.html',
86
                                {'invitations': invitations_for_user(request),
87
                                 'errors': errors},
88
                                context_instance=RequestContext(request))
89
        response =  HttpResponse(data)
90
        _logger.warn("Error adding invitation %s -> %s: %s"%(request.user.uniq,
91
                                                             email, errors))
92
    else:
93
        response = HttpResponseRedirect("/invitations/")
94
        _logger.info("Added invitation %s -> %s"%(request.user.uniq, email))
95

    
96
    return response
97

    
98

    
99
def validate_name(name):
100
    if name is None or name.strip() == '':
101
        raise ValidationError("Name is empty")
102

    
103
    if name.find(' ') is -1:
104
        raise ValidationError("Name must contain at least one space")
105

    
106
    return True
107

    
108

    
109
def invitations_for_user(request):
110
    invitations = []
111

    
112
    for inv in Invitations.objects.filter(source = request.user):
113
        invitation = {}
114

    
115
        invitation['sourcename'] = inv.source.realname
116
        invitation['source'] = inv.source.uniq
117
        invitation['targetname'] = inv.target.realname
118
        invitation['target'] = inv.target.uniq
119
        invitation['accepted'] = inv.accepted
120
        invitation['sent'] = inv.created
121

    
122
        invitations.append(invitation)
123

    
124
    return invitations
125

    
126

    
127
@csrf_protect
128
def inv_demux(request):
129

    
130
    if request.method == 'GET':
131
        data = render_to_string('invitations.html',
132
                                {'invitations': invitations_for_user(request)},
133
                                context_instance=RequestContext(request))
134
        return  HttpResponse(data)
135
    elif request.method == 'POST':
136
        return process_form(request)
137
    else:
138
        method_not_allowed(request)
139

    
140

    
141
def login(request):
142

    
143
    if not request.method == 'GET':
144
        method_not_allowed(request)
145

    
146
    key = request.GET['key']
147

    
148
    if key is None:
149
        return render_login_error("10", "Required key is missing")
150

    
151
    PADDING = '{'
152

    
153
    try:
154
        DecodeAES = lambda c, e: c.decrypt(base64.b64decode(e)).rstrip(PADDING)
155
        cipher = AES.new(settings.INVITATION_ENCR_KEY)
156
        decoded = DecodeAES(cipher, key)
157
    except Exception:
158
        return render_login_error("20", "Required key is invalid")
159

    
160
    users = SynnefoUser.objects.filter(auth_token = decoded)
161

    
162
    if users.count() is 0:
163
        return render_login_error("20", "Required key is invalid")
164

    
165
    user = users[0]
166
    invitations = Invitations.objects.filter(target = user)
167

    
168
    if invitations.count() is 0:
169
        return render_login_error("30", "Non-existent invitation")
170

    
171
    inv = invitations[0]
172

    
173
    valid = timedelta(days = settings.INVITATION_VALID_DAYS)
174
    valid_until = inv.created + valid
175

    
176
    if (time.time() -
177
        time.mktime(inv.created.timetuple()) -
178
        settings.INVITATION_VALID_DAYS * 3600) > 0:
179
        return render_login_error("40",
180
                                  "Invitation expired (was valid until %s)"%
181
                                  valid_until.strftime('%A, %d %B %Y'))
182
    #if inv.accepted == False:
183
    #    return render_login_error("60", "Invitation already accepted")
184

    
185
    inv.accepted = True
186
    inv.save()
187

    
188
    _logger.info("Invited user %s logged in"%(inv.target.uniq))
189

    
190
    data = dict()
191
    data['user'] = user.realname
192
    data['url'] = settings.APP_INSTALL_URL
193

    
194
    welcome = render_to_string('welcome.html', {'data': data})
195

    
196
    response = HttpResponse(welcome)
197

    
198
    response.set_cookie('X-Auth-Token', value=user.auth_token,
199
                        expires = valid_until.strftime('%a, %d-%b-%Y %H:%M:%S %Z'),
200
                        path='/')
201
    response['X-Auth-Token'] = user.auth_token
202
    return response
203

    
204

    
205
def render_login_error(code, text):
206
    error = dict()
207
    error['id'] = code
208
    error['text'] = text
209

    
210
    data = render_to_string('error.html', {'error': error})
211

    
212
    response = HttpResponse(data)
213
    return response
214

    
215

    
216
def send_invitation(invitation):
217
    email = {}
218
    email['invitee'] = invitation.target.realname
219
    email['inviter'] = invitation.source.realname
220

    
221
    valid = timedelta(days = settings.INVITATION_VALID_DAYS)
222
    valid_until = invitation.created + valid
223
    email['valid_until'] = valid_until.strftime('%A, %d %B %Y')
224

    
225
    PADDING = '{'
226
    pad = lambda s: s + (32 - len(s) % 32) * PADDING
227
    EncodeAES = lambda c, s: base64.b64encode(c.encrypt(pad(s)))
228

    
229
    cipher = AES.new(settings.INVITATION_ENCR_KEY)
230
    encoded = EncodeAES(cipher, invitation.target.auth_token)
231

    
232
    url_safe = urllib.urlencode({'key': encoded})
233

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

    
236
    data = render_to_string('invitation.txt', {'email': email})
237

    
238
    send_async(
239
        frm = "%s <%s>"%(invitation.source.realname,invitation.source.uniq),
240
        to = "%s <%s>"%(invitation.target.realname,invitation.target.uniq),
241
        subject = _('Invitation to IaaS service Okeanos'),
242
        body = data
243
    )
244

    
245
def get_invitee_level(source):
246
    return get_user_inv_level(source) + 1
247

    
248

    
249
def get_user_inv_level(u):
250
    inv = Invitations.objects.filter(target = u)
251

    
252
    if not inv:
253
        raise Exception("User without invitation", u)
254

    
255
    return inv[0].level
256

    
257

    
258
@transaction.commit_on_success
259
def add_invitation(source, name, email):
260
    """
261
        Adds an invitation, if the source user has not gone over his/her
262
        invitation limit or the target user has not been invited already
263
    """
264
    num_inv = Invitations.objects.filter(source = source).count()
265

    
266
    if num_inv >= source.max_invitations:
267
        raise TooManyInvitations("User invitation limit (%d) exhausted" %
268
                                 source.max_invitations)
269

    
270
    target = SynnefoUser.objects.filter(uniq = email)
271

    
272
    if target.count() is not 0:
273
        raise AlreadyInvited("User with email %s already invited" % (email))
274

    
275
    users.register_user(name, email)
276

    
277
    target = SynnefoUser.objects.filter(uniq = email)
278

    
279
    r = list(target[:1])
280
    if not r:
281
        raise Exception("Invited user cannot be added")
282

    
283
    u = target[0]
284
    invitee_level = get_invitee_level(source)
285

    
286
    u.max_invitations = settings.INVITATIONS_PER_LEVEL[invitee_level]
287
    u.save()
288

    
289
    inv = Invitations()
290
    inv.source = source
291
    inv.target = u
292
    inv.level = invitee_level
293
    inv.save()
294
    return inv
295

    
296

    
297
@transaction.commit_on_success
298
def invitation_accepted(invitation):
299
    """
300
        Mark an invitation as accepted
301
    """
302
    invitation.accepted = True
303
    invitation.save()
304

    
305
class InvitationException(Exception):
306
    messages = []
307

    
308
class TooManyInvitations(InvitationException):
309

    
310
    def __init__(self, msg):
311
        self.messages.append(msg)
312

    
313

    
314
class AlreadyInvited(InvitationException):
315

    
316
    def __init__(self, msg):
317
        self.messages.append(msg)