Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(6)

Side by Side Diff: third_party/google-endpoints/oauth2client/multistore_file.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2014 Google Inc. All rights reserved.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 """Multi-credential file store with lock support.
16
17 This module implements a JSON credential store where multiple
18 credentials can be stored in one file. That file supports locking
19 both in a single process and across processes.
20
21 The credential themselves are keyed off of:
22
23 * client_id
24 * user_agent
25 * scope
26
27 The format of the stored data is like so::
28
29 {
30 'file_version': 1,
31 'data': [
32 {
33 'key': {
34 'clientId': '<client id>',
35 'userAgent': '<user agent>',
36 'scope': '<scope>'
37 },
38 'credential': {
39 # JSON serialized Credentials.
40 }
41 }
42 ]
43 }
44
45 """
46
47 import errno
48 import json
49 import logging
50 import os
51 import threading
52
53 from oauth2client.client import Credentials
54 from oauth2client.client import Storage as BaseStorage
55 from oauth2client import util
56 from oauth2client.locked_file import LockedFile
57
58
59 __author__ = 'jbeda@google.com (Joe Beda)'
60
61 logger = logging.getLogger(__name__)
62
63 # A dict from 'filename'->_MultiStore instances
64 _multistores = {}
65 _multistores_lock = threading.Lock()
66
67
68 class Error(Exception):
69 """Base error for this module."""
70
71
72 class NewerCredentialStoreError(Error):
73 """The credential store is a newer version than supported."""
74
75
76 @util.positional(4)
77 def get_credential_storage(filename, client_id, user_agent, scope,
78 warn_on_readonly=True):
79 """Get a Storage instance for a credential.
80
81 Args:
82 filename: The JSON file storing a set of credentials
83 client_id: The client_id for the credential
84 user_agent: The user agent for the credential
85 scope: string or iterable of strings, Scope(s) being requested
86 warn_on_readonly: if True, log a warning if the store is readonly
87
88 Returns:
89 An object derived from client.Storage for getting/setting the
90 credential.
91 """
92 # Recreate the legacy key with these specific parameters
93 key = {'clientId': client_id, 'userAgent': user_agent,
94 'scope': util.scopes_to_string(scope)}
95 return get_credential_storage_custom_key(
96 filename, key, warn_on_readonly=warn_on_readonly)
97
98
99 @util.positional(2)
100 def get_credential_storage_custom_string_key(filename, key_string,
101 warn_on_readonly=True):
102 """Get a Storage instance for a credential using a single string as a key.
103
104 Allows you to provide a string as a custom key that will be used for
105 credential storage and retrieval.
106
107 Args:
108 filename: The JSON file storing a set of credentials
109 key_string: A string to use as the key for storing this credential.
110 warn_on_readonly: if True, log a warning if the store is readonly
111
112 Returns:
113 An object derived from client.Storage for getting/setting the
114 credential.
115 """
116 # Create a key dictionary that can be used
117 key_dict = {'key': key_string}
118 return get_credential_storage_custom_key(
119 filename, key_dict, warn_on_readonly=warn_on_readonly)
120
121
122 @util.positional(2)
123 def get_credential_storage_custom_key(filename, key_dict,
124 warn_on_readonly=True):
125 """Get a Storage instance for a credential using a dictionary as a key.
126
127 Allows you to provide a dictionary as a custom key that will be used for
128 credential storage and retrieval.
129
130 Args:
131 filename: The JSON file storing a set of credentials
132 key_dict: A dictionary to use as the key for storing this credential.
133 There is no ordering of the keys in the dictionary. Logically
134 equivalent dictionaries will produce equivalent storage keys.
135 warn_on_readonly: if True, log a warning if the store is readonly
136
137 Returns:
138 An object derived from client.Storage for getting/setting the
139 credential.
140 """
141 multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
142 key = util.dict_to_tuple_key(key_dict)
143 return multistore._get_storage(key)
144
145
146 @util.positional(1)
147 def get_all_credential_keys(filename, warn_on_readonly=True):
148 """Gets all the registered credential keys in the given Multistore.
149
150 Args:
151 filename: The JSON file storing a set of credentials
152 warn_on_readonly: if True, log a warning if the store is readonly
153
154 Returns:
155 A list of the credential keys present in the file. They are returned
156 as dictionaries that can be passed into
157 get_credential_storage_custom_key to get the actual credentials.
158 """
159 multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
160 multistore._lock()
161 try:
162 return multistore._get_all_credential_keys()
163 finally:
164 multistore._unlock()
165
166
167 @util.positional(1)
168 def _get_multistore(filename, warn_on_readonly=True):
169 """A helper method to initialize the multistore with proper locking.
170
171 Args:
172 filename: The JSON file storing a set of credentials
173 warn_on_readonly: if True, log a warning if the store is readonly
174
175 Returns:
176 A multistore object
177 """
178 filename = os.path.expanduser(filename)
179 _multistores_lock.acquire()
180 try:
181 multistore = _multistores.setdefault(
182 filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly))
183 finally:
184 _multistores_lock.release()
185 return multistore
186
187
188 class _MultiStore(object):
189 """A file backed store for multiple credentials."""
190
191 @util.positional(2)
192 def __init__(self, filename, warn_on_readonly=True):
193 """Initialize the class.
194
195 This will create the file if necessary.
196 """
197 self._file = LockedFile(filename, 'r+', 'r')
198 self._thread_lock = threading.Lock()
199 self._read_only = False
200 self._warn_on_readonly = warn_on_readonly
201
202 self._create_file_if_needed()
203
204 # Cache of deserialized store. This is only valid after the
205 # _MultiStore is locked or _refresh_data_cache is called. This is
206 # of the form of:
207 #
208 # ((key, value), (key, value)...) -> OAuth2Credential
209 #
210 # If this is None, then the store hasn't been read yet.
211 self._data = None
212
213 class _Storage(BaseStorage):
214 """A Storage object that can read/write a single credential."""
215
216 def __init__(self, multistore, key):
217 self._multistore = multistore
218 self._key = key
219
220 def acquire_lock(self):
221 """Acquires any lock necessary to access this Storage.
222
223 This lock is not reentrant.
224 """
225 self._multistore._lock()
226
227 def release_lock(self):
228 """Release the Storage lock.
229
230 Trying to release a lock that isn't held will result in a
231 RuntimeError.
232 """
233 self._multistore._unlock()
234
235 def locked_get(self):
236 """Retrieve credential.
237
238 The Storage lock must be held when this is called.
239
240 Returns:
241 oauth2client.client.Credentials
242 """
243 credential = self._multistore._get_credential(self._key)
244 if credential:
245 credential.set_store(self)
246 return credential
247
248 def locked_put(self, credentials):
249 """Write a credential.
250
251 The Storage lock must be held when this is called.
252
253 Args:
254 credentials: Credentials, the credentials to store.
255 """
256 self._multistore._update_credential(self._key, credentials)
257
258 def locked_delete(self):
259 """Delete a credential.
260
261 The Storage lock must be held when this is called.
262
263 Args:
264 credentials: Credentials, the credentials to store.
265 """
266 self._multistore._delete_credential(self._key)
267
268 def _create_file_if_needed(self):
269 """Create an empty file if necessary.
270
271 This method will not initialize the file. Instead it implements a
272 simple version of "touch" to ensure the file has been created.
273 """
274 if not os.path.exists(self._file.filename()):
275 old_umask = os.umask(0o177)
276 try:
277 open(self._file.filename(), 'a+b').close()
278 finally:
279 os.umask(old_umask)
280
281 def _lock(self):
282 """Lock the entire multistore."""
283 self._thread_lock.acquire()
284 try:
285 self._file.open_and_lock()
286 except IOError as e:
287 if e.errno == errno.ENOSYS:
288 logger.warn('File system does not support locking the '
289 'credentials file.')
290 elif e.errno == errno.ENOLCK:
291 logger.warn('File system is out of resources for writing the '
292 'credentials file (is your disk full?).')
293 elif e.errno == errno.EDEADLK:
294 logger.warn('Lock contention on multistore file, opening '
295 'in read-only mode.')
296 else:
297 raise
298 if not self._file.is_locked():
299 self._read_only = True
300 if self._warn_on_readonly:
301 logger.warn('The credentials file (%s) is not writable. '
302 'Opening in read-only mode. Any refreshed '
303 'credentials will only be '
304 'valid for this run.', self._file.filename())
305 if os.path.getsize(self._file.filename()) == 0:
306 logger.debug('Initializing empty multistore file')
307 # The multistore is empty so write out an empty file.
308 self._data = {}
309 self._write()
310 elif not self._read_only or self._data is None:
311 # Only refresh the data if we are read/write or we haven't
312 # cached the data yet. If we are readonly, we assume is isn't
313 # changing out from under us and that we only have to read it
314 # once. This prevents us from whacking any new access keys that
315 # we have cached in memory but were unable to write out.
316 self._refresh_data_cache()
317
318 def _unlock(self):
319 """Release the lock on the multistore."""
320 self._file.unlock_and_close()
321 self._thread_lock.release()
322
323 def _locked_json_read(self):
324 """Get the raw content of the multistore file.
325
326 The multistore must be locked when this is called.
327
328 Returns:
329 The contents of the multistore decoded as JSON.
330 """
331 assert self._thread_lock.locked()
332 self._file.file_handle().seek(0)
333 return json.load(self._file.file_handle())
334
335 def _locked_json_write(self, data):
336 """Write a JSON serializable data structure to the multistore.
337
338 The multistore must be locked when this is called.
339
340 Args:
341 data: The data to be serialized and written.
342 """
343 assert self._thread_lock.locked()
344 if self._read_only:
345 return
346 self._file.file_handle().seek(0)
347 json.dump(data, self._file.file_handle(),
348 sort_keys=True, indent=2, separators=(',', ': '))
349 self._file.file_handle().truncate()
350
351 def _refresh_data_cache(self):
352 """Refresh the contents of the multistore.
353
354 The multistore must be locked when this is called.
355
356 Raises:
357 NewerCredentialStoreError: Raised when a newer client has written
358 the store.
359 """
360 self._data = {}
361 try:
362 raw_data = self._locked_json_read()
363 except Exception:
364 logger.warn('Credential data store could not be loaded. '
365 'Will ignore and overwrite.')
366 return
367
368 version = 0
369 try:
370 version = raw_data['file_version']
371 except Exception:
372 logger.warn('Missing version for credential data store. It may be '
373 'corrupt or an old version. Overwriting.')
374 if version > 1:
375 raise NewerCredentialStoreError(
376 'Credential file has file_version of %d. '
377 'Only file_version of 1 is supported.' % version)
378
379 credentials = []
380 try:
381 credentials = raw_data['data']
382 except (TypeError, KeyError):
383 pass
384
385 for cred_entry in credentials:
386 try:
387 key, credential = self._decode_credential_from_json(cred_entry)
388 self._data[key] = credential
389 except:
390 # If something goes wrong loading a credential, just ignore it
391 logger.info('Error decoding credential, skipping',
392 exc_info=True)
393
394 def _decode_credential_from_json(self, cred_entry):
395 """Load a credential from our JSON serialization.
396
397 Args:
398 cred_entry: A dict entry from the data member of our format
399
400 Returns:
401 (key, cred) where the key is the key tuple and the cred is the
402 OAuth2Credential object.
403 """
404 raw_key = cred_entry['key']
405 key = util.dict_to_tuple_key(raw_key)
406 credential = None
407 credential = Credentials.new_from_json(
408 json.dumps(cred_entry['credential']))
409 return (key, credential)
410
411 def _write(self):
412 """Write the cached data back out.
413
414 The multistore must be locked.
415 """
416 raw_data = {'file_version': 1}
417 raw_creds = []
418 raw_data['data'] = raw_creds
419 for (cred_key, cred) in self._data.items():
420 raw_key = dict(cred_key)
421 raw_cred = json.loads(cred.to_json())
422 raw_creds.append({'key': raw_key, 'credential': raw_cred})
423 self._locked_json_write(raw_data)
424
425 def _get_all_credential_keys(self):
426 """Gets all the registered credential keys in the multistore.
427
428 Returns:
429 A list of dictionaries corresponding to all the keys currently
430 registered
431 """
432 return [dict(key) for key in self._data.keys()]
433
434 def _get_credential(self, key):
435 """Get a credential from the multistore.
436
437 The multistore must be locked.
438
439 Args:
440 key: The key used to retrieve the credential
441
442 Returns:
443 The credential specified or None if not present
444 """
445 return self._data.get(key, None)
446
447 def _update_credential(self, key, cred):
448 """Update a credential and write the multistore.
449
450 This must be called when the multistore is locked.
451
452 Args:
453 key: The key used to retrieve the credential
454 cred: The OAuth2Credential to update/set
455 """
456 self._data[key] = cred
457 self._write()
458
459 def _delete_credential(self, key):
460 """Delete a credential and write the multistore.
461
462 This must be called when the multistore is locked.
463
464 Args:
465 key: The key used to retrieve the credential
466 """
467 try:
468 del self._data[key]
469 except KeyError:
470 pass
471 self._write()
472
473 def _get_storage(self, key):
474 """Get a Storage object to get/set a credential.
475
476 This Storage is a 'view' into the multistore.
477
478 Args:
479 key: The key used to retrieve the credential
480
481 Returns:
482 A Storage object that can be used to get/set this cred
483 """
484 return self._Storage(self, key)
OLDNEW
« no previous file with comments | « third_party/google-endpoints/oauth2client/locked_file.py ('k') | third_party/google-endpoints/oauth2client/service_account.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698