Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-client / astakosclient / __init__.py @ f74d2b69

History | View | Annotate | Download (11.4 kB)

1
# Copyright (C) 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import logging
35
import urlparse
36
import urllib
37
import hashlib
38
from copy import copy
39

    
40
import simplejson
41
from astakosclient.utils import retry, scheme_to_class
42
from astakosclient.errors import \
43
    AstakosClientException, Unauthorized, BadRequest, NotFound, Forbidden, \
44
    NoDisplayName, NoUUID
45

    
46

    
47
# --------------------------------------------------------------------
48
# Astakos Client Class
49

    
50
def getTokenFromCookie(request, cookie_name):
51
    """Extract token from the cookie name provided
52

53
    Cookie should be in the same form as astakos
54
    service sets its cookie contents:
55
        <user_uniq>|<user_token>
56

57
    """
58
    try:
59
        cookie_content = urllib.unquote(request.COOKIE.get(cookie_name, None))
60
        return cookie_content.split("|")[1]
61
    except:
62
        return None
63

    
64

    
65
class AstakosClient():
66
    """AstakosClient Class Implementation"""
67

    
68
    # ----------------------------------
69
    def __init__(self, astakos_url, retry=0,
70
                 use_pool=False, pool_size=8, logger=None):
71
        """Intialize AstakosClient Class
72

73
        Keyword arguments:
74
        astakos_url -- i.e https://accounts.example.com (string)
75
        use_pool    -- use objpool for http requests (boolean)
76
        retry       -- how many time to retry (integer)
77
        logger      -- pass a different logger
78

79
        """
80
        if logger is None:
81
            logging.basicConfig(
82
                format='%(asctime)s [%(levelname)s] %(name)s %(message)s',
83
                datefmt='%Y-%m-%d %H:%M:%S',
84
                level=logging.INFO)
85
            logger = logging.getLogger("astakosclient")
86
        logger.debug("Intialize AstakosClient: astakos_url = %s, "
87
                     "use_pool = %s" % (astakos_url, use_pool))
88

    
89
        if not astakos_url:
90
            m = "Astakos url not given"
91
            logger.error(m)
92
            raise ValueError(m)
93

    
94
        # Check for supported scheme
95
        p = urlparse.urlparse(astakos_url)
96
        conn_class = scheme_to_class(p.scheme, use_pool, pool_size)
97
        if conn_class is None:
98
            m = "Unsupported scheme: %s" % p.scheme
99
            logger.error(m)
100
            raise ValueError(m)
101

    
102
        # Save astakos_url etc. in our class
103
        self.retry = retry
104
        self.logger = logger
105
        self.netloc = p.netloc
106
        self.scheme = p.scheme
107
        self.conn_class = conn_class
108

    
109
    # ----------------------------------
110
    @retry
111
    def _callAstakos(self, token, request_path,
112
                     headers=None, body=None, method="GET"):
113
        """Make the actual call to Astakos Service"""
114
        hashed_token = hashlib.sha1()
115
        hashed_token.update(token)
116
        self.logger.debug(
117
            "Make a %s request to %s using token %s "
118
            "with headers %s and body %s"
119
            % (method, request_path, hashed_token.hexdigest(), headers, body))
120

    
121
        # Check Input
122
        if not token:
123
            m = "Token not given"
124
            self.logger.error(m)
125
            raise ValueError(m)
126
        if headers is None:
127
            headers = {}
128
        if body is None:
129
            body = {}
130
        if request_path[0] != '/':
131
            request_path = "/" + request_path
132

    
133
        # Build request's header and body
134
        kwargs = {}
135
        kwargs['headers'] = copy(headers)
136
        kwargs['headers']['X-Auth-Token'] = token
137
        if body:
138
            kwargs['body'] = copy(body)
139
            kwargs['headers'].setdefault(
140
                'content-type', 'application/octet-stream')
141
        kwargs['headers'].setdefault('content-length',
142
                                     len(body) if body else 0)
143

    
144
        # Get the connection object
145
        conn = self.conn_class(self.netloc)
146

    
147
        # Send request
148
        try:
149
            (data, status) = _doRequest(conn, method, request_path, **kwargs)
150
        except Exception as err:
151
            self.logger.error("Failed to send request: %s" % repr(err))
152
            raise AstakosClientException(str(err))
153
        finally:
154
            conn.close()
155

    
156
        # Return
157
        self.logger.debug("Request returned with status %s" % status)
158
        if status == 400:
159
            raise BadRequest(data)
160
        elif status == 401:
161
            raise Unauthorized(data)
162
        elif status == 403:
163
            raise Forbidden(data)
164
        elif status == 404:
165
            raise NotFound(data)
166
        elif status < 200 or status >= 300:
167
            raise AstakosClientException(data, status)
168
        return simplejson.loads(unicode(data))
169

    
170
    # ------------------------
171
    def getUserInfo(self, token, usage=False):
172
        """Authenticate user and get user's info as a dictionary
173

174
        Keyword arguments:
175
        token   -- user's token (string)
176
        usage   -- return usage information for user (boolean)
177

178
        In case of success return user information (json parsed format).
179
        Otherwise raise an AstakosClientException.
180

181
        """
182
        # Send request
183
        auth_path = "/im/authenticate"
184
        if usage:
185
            auth_path += "?usage=1"
186
        return self._callAstakos(token, auth_path)
187

    
188
    # ----------------------------------
189
    def _uuidCatalog(self, token, uuids, req_path):
190
        req_headers = {'content-type': 'application/json'}
191
        req_body = simplejson.dumps({'uuids': uuids})
192
        data = self._callAstakos(
193
            token, req_path, req_headers, req_body, "POST")
194
        if "uuid_catalog" in data:
195
            return data.get("uuid_catalog")
196
        else:
197
            m = "_uuidCatalog request returned %s. No uuid_catalog found" \
198
                % data
199
            self.logger.error(m)
200
            raise AstakosClientException(m)
201

    
202
    def getDisplayNames(self, token, uuids):
203
        """Return a uuid_catalog dictionary for the given uuids
204

205
        Keyword arguments:
206
        token   -- user's token (string)
207
        uuids   -- list of user ids (list of strings)
208

209
        The returned uuid_catalog is a dictionary with uuids as
210
        keys and the corresponding user names as values
211

212
        """
213
        req_path = "/user_catalogs"
214
        return self._uuidCatalog(token, uuids, req_path)
215

    
216
    def getDisplayName(self, token, uuid):
217
        """Return the displayName of a uuid (see getDisplayNames)"""
218
        if not uuid:
219
            m = "No uuid was given"
220
            self.logger.error(m)
221
            raise ValueError(m)
222
        uuid_dict = self.getDisplayNames(token, [uuid])
223
        if uuid in uuid_dict:
224
            return uuid_dict.get(uuid)
225
        else:
226
            raise NoDisplayName(uuid)
227

    
228
    def getServiceDisplayNames(self, token, uuids):
229
        """Return a uuid_catalog dict using a service's token"""
230
        req_path = "/service/api/user_catalogs"
231
        return self._uuidCatalog(token, uuids, req_path)
232

    
233
    def getServiceDisplayName(self, token, uuid):
234
        """Return the displayName of a uuid using a service's token"""
235
        if not uuid:
236
            m = "No uuid was given"
237
            self.logger.error(m)
238
            raise ValueError(m)
239
        uuid_dict = self.getServiceDisplayNames(token, [uuid])
240
        if uuid in uuid_dict:
241
            return uuid_dict.get(uuid)
242
        else:
243
            raise NoDisplayName(uuid)
244

    
245
    # ----------------------------------
246
    def _displayNameCatalog(self, token, display_names, req_path):
247
        req_headers = {'content-type': 'application/json'}
248
        req_body = simplejson.dumps({'displaynames': display_names})
249
        data = self._callAstakos(
250
            token, req_path, req_headers, req_body, "POST")
251
        if "displayname_catalog" in data:
252
            return data.get("displayname_catalog")
253
        else:
254
            m = "_displayNameCatalog request returned %s. " \
255
                "No displayname_catalog found" % data
256
            self.logger.error(m)
257
            raise AstakosClientException(m)
258

    
259
    def getUUIDs(self, token, display_names):
260
        """Return a displayname_catalog for the given names
261

262
        Keyword arguments:
263
        token           -- user's token (string)
264
        display_names   -- list of user names (list of strings)
265

266
        The returned displayname_catalog is a dictionary with
267
        the names as keys and the corresponding uuids as values
268

269
        """
270
        req_path = "/user_catalogs"
271
        return self._displayNameCatalog(token, display_names, req_path)
272

    
273
    def getUUID(self, token, display_name):
274
        """Return the uuid of a name (see getUUIDs)"""
275
        if not display_name:
276
            m = "No display_name was given"
277
            self.logger.error(m)
278
            raise ValueError(m)
279
        name_dict = self.getUUIDs(token, [display_name])
280
        if display_name in name_dict:
281
            return name_dict.get(display_name)
282
        else:
283
            raise NoUUID(display_name)
284

    
285
    def getServiceUUIDs(self, token, display_names):
286
        """Return a display_name catalog using a service's token"""
287
        req_path = "/service/api/user_catalogs"
288
        return self._displayNameCatalog(token, display_names, req_path)
289

    
290
    def getServiceUUID(self, token, display_name):
291
        """Return the uuid of a name using a service's token"""
292
        if not display_name:
293
            m = "No display_name was given"
294
            self.logger.error(m)
295
            raise ValueError(m)
296
        name_dict = self.getServiceUUIDs(token, [display_name])
297
        if display_name in name_dict:
298
            return name_dict.get(display_name)
299
        else:
300
            raise NoUUID(display_name)
301

    
302
    # ----------------------------------
303
    def getServices(self):
304
        """Return a list of dicts with the registered services"""
305
        return self._callAstakos("dummy token", "/im/get_services")
306

    
307

    
308
# --------------------------------------------------------------------
309
# Private functions
310
# We want _doRequest to be a distinct function
311
# so that we can replace it during unit tests.
312
def _doRequest(conn, method, url, **kwargs):
313
    """The actual request. This function can easily be mocked"""
314
    conn.request(method, url, **kwargs)
315
    response = conn.getresponse()
316
    length = response.getheader('content-length', None)
317
    data = response.read(length)
318
    status = int(response.status)
319
    return (data, status)