1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
|
# Copyright 2012 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Main entry point into the Token Persistence service."""
import abc
import copy
from oslo_config import cfg
from oslo_log import log
from oslo_utils import timeutils
import six
from keystone.common import cache
from keystone.common import dependency
from keystone.common import manager
from keystone import exception
from keystone.i18n import _LW
from keystone.token import utils
CONF = cfg.CONF
LOG = log.getLogger(__name__)
MEMOIZE = cache.get_memoization_decorator(section='token')
REVOCATION_MEMOIZE = cache.get_memoization_decorator(
section='token', expiration_section='revoke')
@dependency.requires('assignment_api', 'identity_api', 'resource_api',
'token_provider_api', 'trust_api')
class PersistenceManager(manager.Manager):
"""Default pivot point for the Token Persistence backend.
See :mod:`keystone.common.manager.Manager` for more details on how this
dynamically calls the backend.
"""
driver_namespace = 'keystone.token.persistence'
def __init__(self):
super(PersistenceManager, self).__init__(CONF.token.driver)
def _assert_valid(self, token_id, token_ref):
"""Raise TokenNotFound if the token is expired."""
current_time = timeutils.normalize_time(timeutils.utcnow())
expires = token_ref.get('expires')
if not expires or current_time > timeutils.normalize_time(expires):
raise exception.TokenNotFound(token_id=token_id)
def get_token(self, token_id):
if not token_id:
# NOTE(morganfainberg): There are cases when the
# context['token_id'] will in-fact be None. This also saves
# a round-trip to the backend if we don't have a token_id.
raise exception.TokenNotFound(token_id='')
unique_id = utils.generate_unique_id(token_id)
token_ref = self._get_token(unique_id)
# NOTE(morganfainberg): Lift expired checking to the manager, there is
# no reason to make the drivers implement this check. With caching,
# self._get_token could return an expired token. Make sure we behave
# as expected and raise TokenNotFound on those instances.
self._assert_valid(token_id, token_ref)
return token_ref
@MEMOIZE
def _get_token(self, token_id):
# Only ever use the "unique" id in the cache key.
return self.driver.get_token(token_id)
def create_token(self, token_id, data):
unique_id = utils.generate_unique_id(token_id)
data_copy = copy.deepcopy(data)
data_copy['id'] = unique_id
ret = self.driver.create_token(unique_id, data_copy)
if MEMOIZE.should_cache(ret):
# NOTE(morganfainberg): when doing a cache set, you must pass the
# same arguments through, the same as invalidate (this includes
# "self"). First argument is always the value to be cached
self._get_token.set(ret, self, unique_id)
return ret
def delete_token(self, token_id):
if not CONF.token.revoke_by_id:
return
unique_id = utils.generate_unique_id(token_id)
self.driver.delete_token(unique_id)
self._invalidate_individual_token_cache(unique_id)
self.invalidate_revocation_list()
def delete_tokens(self, user_id, tenant_id=None, trust_id=None,
consumer_id=None):
if not CONF.token.revoke_by_id:
return
token_list = self.driver.delete_tokens(user_id, tenant_id, trust_id,
consumer_id)
for token_id in token_list:
unique_id = utils.generate_unique_id(token_id)
self._invalidate_individual_token_cache(unique_id)
self.invalidate_revocation_list()
@REVOCATION_MEMOIZE
def list_revoked_tokens(self):
return self.driver.list_revoked_tokens()
def invalidate_revocation_list(self):
# NOTE(morganfainberg): Note that ``self`` needs to be passed to
# invalidate() because of the way the invalidation method works on
# determining cache-keys.
self.list_revoked_tokens.invalidate(self)
def delete_tokens_for_domain(self, domain_id):
"""Delete all tokens for a given domain.
It will delete all the project-scoped tokens for the projects
that are owned by the given domain, as well as any tokens issued
to users that are owned by this domain.
However, deletion of domain_scoped tokens will still need to be
implemented as stated in TODO below.
"""
if not CONF.token.revoke_by_id:
return
projects = self.resource_api.list_projects()
for project in projects:
if project['domain_id'] == domain_id:
for user_id in self.assignment_api.list_user_ids_for_project(
project['id']):
self.delete_tokens_for_user(user_id, project['id'])
# TODO(morganfainberg): implement deletion of domain_scoped tokens.
users = self.identity_api.list_users(domain_id)
user_ids = (user['id'] for user in users)
self.delete_tokens_for_users(user_ids)
def delete_tokens_for_user(self, user_id, project_id=None):
"""Delete all tokens for a given user or user-project combination.
This method adds in the extra logic for handling trust-scoped token
revocations in a single call instead of needing to explicitly handle
trusts in the caller's logic.
"""
if not CONF.token.revoke_by_id:
return
self.delete_tokens(user_id, tenant_id=project_id)
for trust in self.trust_api.list_trusts_for_trustee(user_id):
# Ensure we revoke tokens associated to the trust / project
# user_id combination.
self.delete_tokens(user_id, trust_id=trust['id'],
tenant_id=project_id)
for trust in self.trust_api.list_trusts_for_trustor(user_id):
# Ensure we revoke tokens associated to the trust / project /
# user_id combination where the user_id is the trustor.
# NOTE(morganfainberg): This revocation is a bit coarse, but it
# covers a number of cases such as disabling of the trustor user,
# deletion of the trustor user (for any number of reasons). It
# might make sense to refine this and be more surgical on the
# deletions (e.g. don't revoke tokens for the trusts when the
# trustor changes password). For now, to maintain previous
# functionality, this will continue to be a bit overzealous on
# revocations.
self.delete_tokens(trust['trustee_user_id'], trust_id=trust['id'],
tenant_id=project_id)
def delete_tokens_for_users(self, user_ids, project_id=None):
"""Delete all tokens for a list of user_ids.
:param user_ids: list of user identifiers
:param project_id: optional project identifier
"""
if not CONF.token.revoke_by_id:
return
for user_id in user_ids:
self.delete_tokens_for_user(user_id, project_id=project_id)
def _invalidate_individual_token_cache(self, token_id):
# NOTE(morganfainberg): invalidate takes the exact same arguments as
# the normal method, this means we need to pass "self" in (which gets
# stripped off).
# FIXME(morganfainberg): Does this cache actually need to be
# invalidated? We maintain a cached revocation list, which should be
# consulted before accepting a token as valid. For now we will
# do the explicit individual token invalidation.
self._get_token.invalidate(self, token_id)
self.token_provider_api.invalidate_individual_token_cache(token_id)
@dependency.requires('token_provider_api')
@dependency.provider('token_api')
class Manager(object):
"""The token_api provider.
This class is a proxy class to the token_provider_api's persistence
manager.
"""
def __init__(self):
# NOTE(morganfainberg): __init__ is required for dependency processing.
super(Manager, self).__init__()
def __getattr__(self, item):
"""Forward calls to the `token_provider_api` persistence manager."""
# NOTE(morganfainberg): Prevent infinite recursion, raise an
# AttributeError for 'token_provider_api' ensuring that the dep
# injection doesn't infinitely try and lookup self.token_provider_api
# on _process_dependencies. This doesn't need an exception string as
# it should only ever be hit on instantiation.
if item == 'token_provider_api':
raise AttributeError()
f = getattr(self.token_provider_api._persistence, item)
LOG.warning(_LW('`token_api.%s` is deprecated as of Juno in favor of '
'utilizing methods on `token_provider_api` and may be '
'removed in Kilo.'), item)
setattr(self, item, f)
return f
@six.add_metaclass(abc.ABCMeta)
class TokenDriverV8(object):
"""Interface description for a Token driver."""
@abc.abstractmethod
def get_token(self, token_id):
"""Get a token by id.
:param token_id: identity of the token
:type token_id: string
:returns: token_ref
:raises: keystone.exception.TokenNotFound
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def create_token(self, token_id, data):
"""Create a token by id and data.
:param token_id: identity of the token
:type token_id: string
:param data: dictionary with additional reference information
::
{
expires=''
id=token_id,
user=user_ref,
tenant=tenant_ref,
metadata=metadata_ref
}
:type data: dict
:returns: token_ref or None.
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def delete_token(self, token_id):
"""Deletes a token by id.
:param token_id: identity of the token
:type token_id: string
:returns: None.
:raises: keystone.exception.TokenNotFound
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def delete_tokens(self, user_id, tenant_id=None, trust_id=None,
consumer_id=None):
"""Deletes tokens by user.
If the tenant_id is not None, only delete the tokens by user id under
the specified tenant.
If the trust_id is not None, it will be used to query tokens and the
user_id will be ignored.
If the consumer_id is not None, only delete the tokens by consumer id
that match the specified consumer id.
:param user_id: identity of user
:type user_id: string
:param tenant_id: identity of the tenant
:type tenant_id: string
:param trust_id: identity of the trust
:type trust_id: string
:param consumer_id: identity of the consumer
:type consumer_id: string
:returns: The tokens that have been deleted.
:raises: keystone.exception.TokenNotFound
"""
if not CONF.token.revoke_by_id:
return
token_list = self._list_tokens(user_id,
tenant_id=tenant_id,
trust_id=trust_id,
consumer_id=consumer_id)
for token in token_list:
try:
self.delete_token(token)
except exception.NotFound:
pass
return token_list
@abc.abstractmethod
def _list_tokens(self, user_id, tenant_id=None, trust_id=None,
consumer_id=None):
"""Returns a list of current token_id's for a user
This is effectively a private method only used by the ``delete_tokens``
method and should not be called by anything outside of the
``token_api`` manager or the token driver itself.
:param user_id: identity of the user
:type user_id: string
:param tenant_id: identity of the tenant
:type tenant_id: string
:param trust_id: identity of the trust
:type trust_id: string
:param consumer_id: identity of the consumer
:type consumer_id: string
:returns: list of token_id's
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def list_revoked_tokens(self):
"""Returns a list of all revoked tokens
:returns: list of token_id's
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def flush_expired_tokens(self):
"""Archive or delete tokens that have expired.
"""
raise exception.NotImplemented() # pragma: no cover
Driver = manager.create_legacy_driver(TokenDriverV8)
|