Statistics
| Branch: | Tag: | Revision:

root / invitations / invitations.py @ 68a77896

History | View | Annotate | Download (9.6 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
    else:
91
        response = HttpResponseRedirect("/invitations/")
92

    
93
    return response
94

    
95

    
96
def validate_name(name):
97
    if name is None or name.strip() == '':
98
        raise ValidationError("Name is empty")
99

    
100
    if name.find(' ') is -1:
101
        raise ValidationError("Name must contain at least one space")
102

    
103
    return True
104

    
105

    
106
def invitations_for_user(request):
107
    invitations = []
108

    
109
    for inv in Invitations.objects.filter(source = request.user):
110
        invitation = {}
111

    
112
        invitation['sourcename'] = inv.source.realname
113
        invitation['source'] = inv.source.uniq
114
        invitation['targetname'] = inv.target.realname
115
        invitation['target'] = inv.target.uniq
116
        invitation['accepted'] = inv.accepted
117
        invitation['sent'] = inv.created
118

    
119
        invitations.append(invitation)
120

    
121
    return invitations
122

    
123

    
124
@csrf_protect
125
def inv_demux(request):
126

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

    
137

    
138
def login(request):
139

    
140
    if not request.method == 'GET':
141
        method_not_allowed(request)
142

    
143
    key = request.GET['key']
144

    
145
    if key is None:
146
        return render_login_error("10", "Required key is missing")
147

    
148
    PADDING = '{'
149

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

    
157
    users = SynnefoUser.objects.filter(auth_token = decoded)
158

    
159
    if users.count() is 0:
160
        return render_login_error("20", "Required key is invalid")
161

    
162
    user = users[0]
163
    invitations = Invitations.objects.filter(target = user)
164

    
165
    if invitations.count() is 0:
166
        return render_login_error("30", "Non-existent invitation")
167

    
168
    inv = invitations[0]
169

    
170
    valid = timedelta(days = settings.INVITATION_VALID_DAYS)
171
    valid_until = inv.created + valid
172

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

    
182
    inv.accepted = True
183
    inv.save()
184

    
185
    data = dict()
186
    data['user'] = user.realname
187
    data['url'] = settings.APP_INSTALL_URL
188

    
189
    welcome = render_to_string('welcome.html', {'data': data})
190

    
191
    response = HttpResponse(welcome)
192

    
193
    response.set_cookie('X-Auth-Token', value=user.auth_token,
194
                        expires = valid_until.strftime('%a, %d-%b-%Y %H:%M:%S %Z'),
195
                        path='/')
196
    response['X-Auth-Token'] = user.auth_token
197
    return response
198

    
199

    
200
def render_login_error(code, text):
201
    error = dict()
202
    error['id'] = code
203
    error['text'] = text
204

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

    
207
    response = HttpResponse(data)
208
    return response
209

    
210

    
211
def send_invitation(invitation):
212
    email = {}
213
    email['invitee'] = invitation.target.realname
214
    email['inviter'] = invitation.source.realname
215

    
216
    valid = timedelta(days = settings.INVITATION_VALID_DAYS)
217
    valid_until = invitation.created + valid
218
    email['valid_until'] = valid_until.strftime('%A, %d %B %Y')
219

    
220
    PADDING = '{'
221
    pad = lambda s: s + (32 - len(s) % 32) * PADDING
222
    EncodeAES = lambda c, s: base64.b64encode(c.encrypt(pad(s)))
223

    
224
    cipher = AES.new(settings.INVITATION_ENCR_KEY)
225
    encoded = EncodeAES(cipher, invitation.target.auth_token)
226

    
227
    url_safe = urllib.urlencode({'key': encoded})
228

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

    
231
    data = render_to_string('invitation.txt', {'email': email})
232

    
233
    send_async(
234
        frm = "%s <%s>"%(invitation.source.realname,invitation.source.uniq),
235
        to = "%s <%s>"%(invitation.target.realname,invitation.target.uniq),
236
        subject = _('Invitation to IaaS service Okeanos'),
237
        body = data
238
    )
239

    
240
def get_invitee_level(source):
241
    return get_user_inv_level(source) + 1
242

    
243

    
244
def get_user_inv_level(u):
245
    inv = Invitations.objects.filter(target = u)
246

    
247
    if inv is None:
248
        raise Exception("User without invitation", u)
249

    
250
    return inv[0].level
251

    
252

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

    
261
    if num_inv >= source.max_invitations:
262
        raise TooManyInvitations("User invitation limit (%d) exhausted" %
263
                                 source.max_invitations)
264

    
265
    target = SynnefoUser.objects.filter(uniq = email)
266

    
267
    if target.count() is not 0:
268
        raise AlreadyInvited("User with email %s already invited" % (email))
269

    
270
    users.register_user(name, email)
271

    
272
    target = SynnefoUser.objects.filter(uniq = email)
273

    
274
    r = list(target[:1])
275
    if not r:
276
        raise Exception("Invited user cannot be added")
277

    
278
    u = target[0]
279
    invitee_level = get_invitee_level(source)
280

    
281
    u.max_invitations = settings.INVITATIONS_PER_LEVEL[invitee_level]
282
    u.save()
283

    
284
    inv = Invitations()
285
    inv.source = source
286
    inv.target = u
287
    inv.level = invitee_level
288
    inv.save()
289
    return inv
290

    
291

    
292
@transaction.commit_on_success
293
def invitation_accepted(invitation):
294
    """
295
        Mark an invitation as accepted
296
    """
297
    invitation.accepted = True
298
    invitation.save()
299

    
300
class InvitationException(Exception):
301
    messages = []
302

    
303
class TooManyInvitations(InvitationException):
304

    
305
    def __init__(self, msg):
306
        self.messages.append(msg)
307

    
308

    
309
class AlreadyInvited(InvitationException):
310

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