Statistics
| Branch: | Tag: | Revision:

root / invitations / invitations.py @ d028ab18

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 gettext_lazy 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 = u'Πρόσκληση στην υπηρεσία Ωκεανός',
237
        #subject = _('Invitation to IaaS service Okeanos'),
238
        body = data
239
    )
240

    
241

    
242
def get_invitee_level(source):
243
    return get_user_inv_level(source) + 1
244

    
245

    
246
def get_user_inv_level(u):
247
    inv = Invitations.objects.filter(target = u)
248

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

    
252
    return inv[0].level
253

    
254

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

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

    
267
    target = SynnefoUser.objects.filter(uniq = email)
268

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

    
272
    users.register_user(name, email)
273

    
274
    target = SynnefoUser.objects.filter(uniq = email)
275

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

    
280
    u = target[0]
281
    invitee_level = get_invitee_level(source)
282

    
283
    u.max_invitations = settings.INVITATIONS_PER_LEVEL[invitee_level]
284
    u.save()
285

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

    
293

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

    
302
class InvitationException(Exception):
303
    messages = []
304

    
305
class TooManyInvitations(InvitationException):
306

    
307
    def __init__(self, msg):
308
        self.messages.append(msg)
309

    
310

    
311
class AlreadyInvited(InvitationException):
312

    
313
    def __init__(self, msg):
314
        self.messages.append(msg)