1#!/usr/bin/env python
2# encoding: utf8
3#
4# Copyright © Burak Arslan <burak at arskom dot com dot tr>,
5#             Arskom Ltd. http://www.arskom.com.tr
6# All rights reserved.
7#
8# Redistribution and use in source and binary forms, with or without
9# modification, are permitted provided that the following conditions are met:
10#
11#    1. Redistributions of source code must retain the above copyright notice,
12#       this list of conditions and the following disclaimer.
13#    2. Redistributions in binary form must reproduce the above copyright
14#       notice, this list of conditions and the following disclaimer in the
15#       documentation and/or other materials provided with the distribution.
16#    3. Neither the name of the owner nor the names of its contributors may be
17#       used to endorse or promote products derived from this software without
18#       specific prior written permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT,
24# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
25# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
27# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
29# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30#
31
32
33import logging
34import random
35import sys
36
37# bcrypt seems to be among the latest consensus around cryptograpic circles on
38# storing passwords.
39# You need the package from http://code.google.com/p/py-bcrypt/
40# You can install it by running easy_install py-bcrypt.
41try:
42    import bcrypt
43except ImportError:
44    print('easy_install --user py-bcrypt to get it.')
45    raise
46
47from spyne.application import Application
48from spyne.decorator import rpc
49from spyne.error import ArgumentError
50from spyne.model.complex import ComplexModel
51from spyne.model.fault import Fault
52from spyne.model.primitive import Mandatory
53from spyne.model.primitive import String
54from spyne.protocol.soap import Soap11
55from spyne.server.wsgi import WsgiApplication
56from spyne.service import Service
57
58
59class PublicKeyError(Fault):
60    __namespace__ = 'spyne.examples.authentication'
61
62    def __init__(self, value):
63        super(PublicKeyError, self).__init__(
64                                       faultstring='Value %r not found' % value)
65
66
67class AuthenticationError(Fault):
68    __namespace__ = 'spyne.examples.authentication'
69
70    def __init__(self, user_name):
71        # TODO: self.transport.http.resp_code = HTTP_401
72
73        super(AuthenticationError, self).__init__(
74                faultcode='Client.AuthenticationError',
75                faultstring='Invalid authentication request for %r' % user_name)
76
77
78class AuthorizationError(Fault):
79    __namespace__ = 'spyne.examples.authentication'
80
81    def __init__(self):
82        # TODO: self.transport.http.resp_code = HTTP_401
83
84        super(AuthorizationError, self).__init__(
85                   faultcode='Client.AuthorizationError',
86                   faultstring='You are not authozied to access this resource.')
87
88
89class SpyneDict(dict):
90    def __getitem__(self, key):
91        try:
92            return dict.__getitem__(self, key)
93        except KeyError:
94            raise PublicKeyError(key)
95
96
97class RequestHeader(ComplexModel):
98    __namespace__ = 'spyne.examples.authentication'
99
100    session_id = Mandatory.String
101    user_name = Mandatory.String
102
103
104class Preferences(ComplexModel):
105    __namespace__ = 'spyne.examples.authentication'
106
107    language = String(max_len=2)
108    time_zone = String
109
110
111user_db = {
112    'neo': bcrypt.hashpw('Wh1teR@bbit', bcrypt.gensalt()),
113}
114
115session_db = set()
116
117preferences_db = SpyneDict({
118    'neo': Preferences(language='en', time_zone='Underground/Zion'),
119    'smith': Preferences(language='xx', time_zone='Matrix/Core'),
120})
121
122
123class AuthenticationService(Service):
124    __tns__ = 'spyne.examples.authentication'
125
126    @rpc(Mandatory.String, Mandatory.String, _returns=String,
127                                                    _throws=AuthenticationError)
128    def authenticate(ctx, user_name, password):
129        password_hash = user_db.get(user_name, None)
130
131        if password_hash is None:
132            raise AuthenticationError(user_name)
133
134        if bcrypt.hashpw(password, password_hash) == password_hash:
135            session_id = (user_name,
136                                '%x' % random.randint(1 << 124, (1 << 128) - 1))
137            session_db.add(session_id)
138
139        else:
140            raise AuthenticationError(user_name)
141
142        return session_id[1]
143
144
145class UserService(Service):
146    __tns__ = 'spyne.examples.authentication'
147    __in_header__ = RequestHeader
148
149    @rpc(Mandatory.String, _throws=PublicKeyError, _returns=Preferences)
150    def get_preferences(ctx, user_name):
151        if user_name == 'smith':
152            raise AuthorizationError()
153
154        retval = preferences_db[user_name]
155
156        return retval
157
158
159def _on_method_call(ctx):
160    if ctx.in_object is None:
161        raise ArgumentError("RequestHeader is null")
162    if not (ctx.in_header.user_name, ctx.in_header.session_id) in session_db:
163        raise AuthenticationError(ctx.in_object.user_name)
164
165
166UserService.event_manager.add_listener('method_call', _on_method_call)
167
168if __name__ == '__main__':
169    from spyne.util.wsgi_wrapper import run_twisted
170
171    logging.basicConfig(level=logging.DEBUG)
172    logging.getLogger('spyne.protocol.xml').setLevel(logging.DEBUG)
173    logging.getLogger('twisted').setLevel(logging.DEBUG)
174
175    application = Application([AuthenticationService, UserService],
176        tns='spyne.examples.authentication',
177        in_protocol=Soap11(validator='lxml'),
178        out_protocol=Soap11()
179    )
180
181    twisted_apps = [
182        (WsgiApplication(application), 'app'),
183    ]
184
185    sys.exit(run_twisted(twisted_apps, 8000))
186