Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / oa2 / backends / base.py @ 3fc7fd80

History | View | Annotate | Download (14 kB)

1
import urllib
2
import urlparse
3
import uuid
4
import datetime
5
import json
6

    
7
from base64 import b64encode, b64decode
8
from hashlib import sha512
9

    
10
import logging
11
logger = logging.getLogger(__name__)
12

    
13

    
14
def handles_oa2_requests(func):
15
    def wrapper(self, *args, **kwargs):
16
        if not self._errors_to_http:
17
            return func(self, *args, **kwargs)
18
        try:
19
            return func(self, *args, **kwargs)
20
        except OA2Error, e:
21
            return self.build_response_from_error(e)
22
    return wrapper
23

    
24

    
25
class OA2Error(Exception):
26
    error = None
27

    
28

    
29
class InvalidClientID(OA2Error):
30
    pass
31

    
32

    
33
class NotAuthenticatedError(OA2Error):
34
    pass
35

    
36

    
37
class InvalidClientRedirectUrl(OA2Error):
38
    pass
39

    
40

    
41
class InvalidAuthorizationRequest(OA2Error):
42
    pass
43

    
44

    
45
class Response(object):
46

    
47
    def __init__(self, status, body='', headers=None,
48
                 content_type='plain/text'):
49
        if not body:
50
            body = ''
51
        if not headers:
52
            headers = {}
53

    
54
        self.status = status
55
        self.body = body
56
        self.headers = headers
57
        self.content_type = content_type
58

    
59
    def __repr__(self):
60
        return "%d RESPONSE (BODY: %r, HEADERS: %r)" % (self.status,
61
                                                        self.body,
62
                                                        self.headers)
63

    
64

    
65
class Request(object):
66

    
67
    def __init__(self, method, GET=None, POST=None, META=None, secure=False,
68
                 user=None):
69
        self.method = method
70

    
71
        if not GET:
72
            GET = {}
73
        if not POST:
74
            POST = {}
75
        if not META:
76
            META = {}
77

    
78
        self.secure = secure
79
        self.GET = GET
80
        self.POST = POST
81
        self.META = META
82
        self.user = user
83

    
84
    def __repr__(self):
85
        prepend = ""
86
        if self.secure:
87
            prepend = "SECURE "
88
        return "%s%s REQUEST (POST: %r, GET:%r, HEADERS:%r, " % (prepend,
89
                                                                 self.method,
90
                                                                 self.POST,
91
                                                                 self.GET,
92
                                                                 self.META)
93

    
94

    
95
class ORMAbstractBase(type):
96

    
97
    def __new__(cls, name, bases, attrs):
98
        attrs['ENTRIES'] = {}
99
        return super(ORMAbstractBase, cls).__new__(cls, name, bases, attrs)
100

    
101

    
102
class ORMAbstract(object):
103

    
104
    ENTRIES = {}
105

    
106
    __metaclass__ = ORMAbstractBase
107

    
108
    def __init__(self, **kwargs):
109
        for key, value in kwargs.iteritems():
110
            setattr(self, key, value)
111

    
112
    @classmethod
113
    def create(cls, id, **params):
114
        params = cls.clean_params(params)
115
        params['id'] = id
116
        cls.ENTRIES[id] = cls(**params)
117
        return cls.get(id)
118

    
119
    @classmethod
120
    def get(cls, pk):
121
        return cls.ENTRIES.get(pk)
122

    
123
    @classmethod
124
    def clean_params(cls, params):
125
        return params
126

    
127

    
128
class Client(ORMAbstract):
129

    
130
    def get_id(self):
131
        return self.id
132

    
133
    def get_redirect_uris(self):
134
        return self.uris
135

    
136
    def get_default_redirect_uri(self):
137
        return self.uris[0]
138

    
139
    def redirect_uri_is_valid(self, redirect_uri):
140
        split = urlparse.urlsplit(redirect_uri)
141
        if split.scheme not in urlparse.uses_query:
142
            raise OA2Error("Invalid redirect url scheme")
143
        uris = self.get_redirect_uris()
144
        return redirect_uri in uris
145

    
146
    def requires_auth(self):
147
        if self.client_type == 'confidential':
148
            return True
149
        return 'secret' in dir(self)
150

    
151
    def check_credentials(self, username, secret):
152
        return username == self.id and secret == self.secret
153

    
154

    
155
class Token(ORMAbstract):
156

    
157
    def to_dict(self):
158
        params = {
159
            'access_token': self.token,
160
            'token_type': self.token_type,
161
            'expires_in': self.expires,
162
        }
163
        if self.refresh_token:
164
            params['refresh_token'] = self.refresh_token
165
        return params
166

    
167

    
168
class AuthorizationCode(ORMAbstract):
169
    pass
170

    
171

    
172
class User(ORMAbstract):
173
    pass
174

    
175

    
176
class BackendBase(type):
177

    
178
    def __new__(cls, name, bases, attrs):
179
        super_new = super(BackendBase, cls).__new__
180
        parents = [b for b in bases if isinstance(b, BackendBase)]
181
        meta = attrs.pop('Meta', None)
182
        return super_new(cls, name, bases, attrs)
183

    
184
    @classmethod
185
    def get_orm_options(cls, attrs):
186
        meta = attrs.pop('ORM', None)
187
        orm = {}
188
        if meta:
189
            for attr in dir(meta):
190
                orm[attr] = getattr(meta, attr)
191
        return orm
192

    
193

    
194
class SimpleBackend(object):
195

    
196
    __metaclass__ = BackendBase
197

    
198
    base_url = ''
199
    endpoints_prefix = '/oa2/'
200

    
201
    token_endpoint = 'token/'
202
    token_length = 30
203
    token_expires = 3600
204

    
205
    authorization_endpoint = 'auth/'
206
    authorization_code_length = 60
207
    authorization_response_types = ['code', 'token']
208

    
209
    grant_types = ['authorization_code', 'implicit']
210

    
211
    response_cls = Response
212
    request_cls = Request
213

    
214
    client_model = Client
215
    token_model = Token
216
    code_model = AuthorizationCode
217
    user_model = User
218

    
219
    def __init__(self, base_url='', endpoints_prefix='/oa2/', id='oa2',
220
                 **kwargs):
221
        self.base_url = base_url
222
        self.endpoints_prefix = endpoints_prefix
223
        self.id = id
224
        self._errors_to_http = kwargs.get('errors_to_http', True)
225

    
226
    # Request/response builders
227
    def build_request(self, method, get, post, meta):
228
        return self.request_cls(method=method, GET=get, POST=post, META=meta)
229

    
230
    def build_response(self, status, headers=None, body=''):
231
        return self.response_cls(status=status, headers=headers, body=body)
232

    
233
    # ORM Methods
234
    def create_authorization_code(self, client, code, redirect_uri, scope,
235
                                  state, **kwargs):
236
        code_params = {
237
            'code': code,
238
            'redirect_uri': redirect_uri,
239
            'client_id': client.get_id(),
240
            'scope': scope,
241
            'state': state
242
        }
243
        code_params.update(kwargs)
244
        return self.code_model.create(code, **code_params)
245

    
246
    def _token_params(self, value, token_type, client, scope):
247
        created_at = datetime.datetime.now()
248
        expires = self.token_expires
249
        expires_at = created_at + datetime.timedelta(seconds=expires)
250
        token_params = {
251
            'token': value,
252
            'token_type': token_type,
253
            'client': client,
254
            'scope': scope,
255
            'created_at': created_at,
256
            'expires': expires,
257
            'expires_at': expires_at
258
        }
259
        return token_params
260

    
261
    def create_token(self, value, token_type, client, scope, refresh=False):
262
        params = self._token_params(value, token_type, client, scope)
263
        if refresh:
264
            refresh_token = self.generate_token()
265
            params['refresh_token'] = refresh_token
266
            # TODO: refresh token expires ???
267
        token = self.token_model.create(value, **params)
268

    
269
    def delete_authorization_code(self, code):
270
        del self.code_model.ENTRIES[code]
271

    
272
    def get_client_by_id(self, client_id):
273
        return self.client_model.get(client_id)
274

    
275
    def get_client_by_credentials(self, username, password):
276
        return None
277

    
278
    def get_authorization_code(self, code):
279
        return self.code_model.get(code)
280

    
281
    def get_client_authorization_code(self, client, code):
282
        code_instance = self.get_authorization_code(code)
283
        if not code_instance:
284
            raise OA2Error("Invalid code", code)
285

    
286
        if client.id != code_instance.client_id:
287
            raise OA2Error("Invalid code for client", code, client)
288
        return code_instance
289

    
290
    def client_id_exists(self, client_id):
291
        return bool(self.get_client_by_id(client_id))
292

    
293
    def build_site_url(self, prefix='', **params):
294
        params = urllib.urlencode(params)
295
        return "%s%s%s%s" % (self.base_url, self.endpoints_prefix, prefix,
296
                             params)
297

    
298
    def _get_uri_base(self, uri):
299
        split = urlparse.urlsplit(uri)
300
        return "%s://%s%s" % (split.scheme, split.netloc, split.path)
301

    
302
    def build_client_redirect_uri(self, client, uri, **params):
303
        if not client.redirect_uri_is_valid(uri):
304
            raise OA2Error("Invalid redirect uri")
305
        params = urllib.urlencode(params)
306
        uri = self._get_uri_base(uri)
307
        return "%s?%s" % (uri, params)
308

    
309
    def generate_authorization_code(self):
310
        dg64 = b64encode(sha512(str(uuid.uuid4())).hexdigest())
311
        return dg64[:self.authorization_code_length]
312

    
313
    def generate_token(self, *args, **kwargs):
314
        dg64 = b64encode(sha512(str(uuid.uuid4())).hexdigest())
315
        return dg64[:self.token_length]
316

    
317
    def add_authorization_code(self, user, client, redirect_uri, scope, state,
318
                               **kwargs):
319
        code = self.generate_authorization_code()
320
        self.create_authorization_code(user, client, code, redirect_uri, scope,
321
                                       state, **kwargs)
322
        return code
323

    
324
    #
325
    # Response helpers
326
    #
327

    
328
    def grant_accept_response(self, client, redirect_uri, scope, state,
329
                              request):
330
        context = {'client': client.get_id(), 'redirect_uri': redirect_uri,
331
                   'scope': scope, 'state': state, 'url': url}
332
        json_content = json.dumps(context)
333
        return self.response_cls(status=200, body=json_content)
334

    
335
    def build_redirect_to_login_response(self, request):
336
        return Response(302, headers={'Location': '/login'})
337

    
338
    def build_response_from_error(self, exception):
339
        response = Response(400)
340
        logger.exception(exception)
341
        error = 'generic_error'
342
        if exception.error:
343
            error = exception.error
344
        body = {
345
            'error': error,
346
            'exception': exception.message,
347
        }
348
        response.body = json.dumps(body)
349
        response.content_type = "application/json"
350
        return response
351

    
352
    #
353
    # Processor methods
354
    #
355

    
356
    def grant_authorization_code(self, user, client, code, redirect_uri,
357
                                 scope):
358
        code = self.get_client_authorization_code(client, code)
359
        if code.scope != scope:
360
            raise OA2Error("Invalid scope")
361
        token = self.add_token_for_client(client, "Bearer", code.scope,
362
                                          refresh=True)
363
        self.delete_authorization_code(code.code)
364
        return token
365

    
366

    
367
    #
368
    # Helpers
369
    #
370

    
371
    def _get_credentials(self, params, headers):
372
        if 'HTTP_AUTHORIZATION' in headers:
373
            scheme, b64credentials = headers.get(
374
                'HTTP_AUTHORIZATION').split(" ")
375
            credentials = b64decode(b64credentials).split(":")
376
            return scheme, credentials
377
        else:
378
            return None, None
379
        pass
380

    
381
    def get_redirect_uri_from_params(self, client, params, default=True):
382
        """
383
        Accepts a client instance and request parameters.
384
        """
385
        redirect_uri = params.get('redirect_uri', None)
386
        if not redirect_uri and default:
387
            redirect_uri = client.get_default_redirect_uri()
388
        else:
389
            # TODO: sanitize redirect_uri (self.clean_redirect_uri ???)
390
            # clean and validate
391
            if not client.redirect_uri_is_valid(redirect_uri):
392
                raise OA2Error("Invalid client redirect uri")
393
        return redirect_uri
394

    
395
    #
396
    # Parameters validation methods
397
    #
398

    
399
    def validate_client(params, meta, requires_auth=False):
400
        raise OA2Error("Invalid client")
401

    
402
    def validate_redirect_uri(client, params, headers, allow_default=True):
403
        raise OA2Error("Invalid redirect uri")
404

    
405
    def validate_state(params, meta):
406
        raise OA2Error("Invalid state")
407

    
408
    def validate_scope(client, params, headers):
409
        raise OA2Error("Invalid state")
410

    
411
    #
412
    # Requests validation methods
413
    #
414

    
415
    def validate_code_request(params, headers):
416
        client = self.validate_client(params, headers, False)
417
        redirect_uri = self.validate_redirect_uri(client, params, headers)
418
        scope = self.validate_scope(client, params, headers)
419
        state = self.validate_state(client, params, headers)
420
        return client, redirect_uri, scope, state
421

    
422
    def validate_token_request(params, headers):
423
        client = self.validate_client(params, headers, False)
424
        redirect_uri = self.validate_redirect_uri(client, params, headers)
425
        scope = self.validate_scope(client, params, headers)
426
        state = self.validate_state(client, params, headers)
427
        return client, redirect_uri, scope, state
428

    
429
    #
430
    # Endpoint methods
431
    #
432

    
433
    @handles_oa2_requests
434
    def authorize(self, request, **extra):
435
        """
436
        Used in the following cases
437
        """
438
        if not request.secure:
439
            raise OA2Error("Secure request required")
440

    
441
        # identify
442
        request_params = request.GET
443
        if request.method == "POST":
444
            request_params = request.POST
445

    
446
        auth_type, params = self.identify_authorize_request(request_params,
447
                                                            request.META)
448

    
449
        if auth_type == 'code':
450
            client, uri, scope, state = \
451
                    self.validate_code_request(params, request.META)
452
        elif auth_type == 'token':
453
            client, uri, scope, state = \
454
                self.validate_token_request(params, request.META)
455
        else:
456
            #TODO: handle custom type
457
            raise OA2Error("Invalid authorization type")
458

    
459
        user = self.get_user_from_request(request)
460
        if not user:
461
            return self.redirect_to_login_response(request)
462

    
463
        if request.method == 'POST':
464
            if auth_type == 'code':
465
                return self.process_code_request(user, client, uri, scope,
466
                                                 state)
467
            elif auth_type == 'token':
468
                return self.process_code_request(user, client, uri, scope,
469
                                                 state)
470
            else:
471
                #TODO: handle custom type
472
                raise OA2Error("Invalid authorization type")
473
        else:
474
            return self.grant_accept_response()