OpenStack Identity (Keystone)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

715 lines
27KB

  1. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  2. # not use this file except in compliance with the License. You may obtain
  3. # a copy of the License at
  4. #
  5. # http://www.apache.org/licenses/LICENSE-2.0
  6. #
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  9. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  10. # License for the specific language governing permissions and limitations
  11. # under the License.
  12. import base64
  13. import datetime
  14. import struct
  15. import uuid
  16. from cryptography import fernet
  17. import msgpack
  18. from oslo_log import log
  19. from oslo_utils import timeutils
  20. import six
  21. from six.moves import map
  22. from six.moves import urllib
  23. from keystone.auth import plugins as auth_plugins
  24. from keystone.common import fernet_utils as utils
  25. from keystone.common import utils as ks_utils
  26. import keystone.conf
  27. from keystone import exception
  28. from keystone.i18n import _, _LI
  29. CONF = keystone.conf.CONF
  30. LOG = log.getLogger(__name__)
  31. # Fernet byte indexes as computed by pypi/keyless_fernet and defined in
  32. # https://github.com/fernet/spec
  33. TIMESTAMP_START = 1
  34. TIMESTAMP_END = 9
  35. class TokenFormatter(object):
  36. """Packs and unpacks payloads into tokens for transport."""
  37. @property
  38. def crypto(self):
  39. """Return a cryptography instance.
  40. You can extend this class with a custom crypto @property to provide
  41. your own token encoding / decoding. For example, using a different
  42. cryptography library (e.g. ``python-keyczar``) or to meet arbitrary
  43. security requirements.
  44. This @property just needs to return an object that implements
  45. ``encrypt(plaintext)`` and ``decrypt(ciphertext)``.
  46. """
  47. fernet_utils = utils.FernetUtils(
  48. CONF.fernet_tokens.key_repository,
  49. CONF.fernet_tokens.max_active_keys,
  50. 'fernet_tokens'
  51. )
  52. keys = fernet_utils.load_keys()
  53. if not keys:
  54. raise exception.KeysNotFound()
  55. fernet_instances = [fernet.Fernet(key) for key in keys]
  56. return fernet.MultiFernet(fernet_instances)
  57. def pack(self, payload):
  58. """Pack a payload for transport as a token.
  59. :type payload: six.binary_type
  60. :rtype: six.text_type
  61. """
  62. # base64 padding (if any) is not URL-safe
  63. return self.crypto.encrypt(payload).rstrip(b'=').decode('utf-8')
  64. def unpack(self, token):
  65. """Unpack a token, and validate the payload.
  66. :type token: six.text_type
  67. :rtype: six.binary_type
  68. """
  69. # TODO(lbragstad): Restore padding on token before decoding it.
  70. # Initially in Kilo, Fernet tokens were returned to the user with
  71. # padding appended to the token. Later in Liberty this padding was
  72. # removed and restored in the Fernet provider. The following if
  73. # statement ensures that we can validate tokens with and without token
  74. # padding, in the event of an upgrade and the tokens that are issued
  75. # throughout the upgrade. Remove this if statement when Mitaka opens
  76. # for development and exclusively use the restore_padding() class
  77. # method.
  78. if token.endswith('%3D'):
  79. token = urllib.parse.unquote(token)
  80. else:
  81. token = TokenFormatter.restore_padding(token)
  82. try:
  83. return self.crypto.decrypt(token.encode('utf-8'))
  84. except fernet.InvalidToken:
  85. raise exception.ValidationError(
  86. _('This is not a recognized Fernet token %s') % token)
  87. @classmethod
  88. def restore_padding(cls, token):
  89. """Restore padding based on token size.
  90. :param token: token to restore padding on
  91. :type token: six.text_type
  92. :returns: token with correct padding
  93. """
  94. # Re-inflate the padding
  95. mod_returned = len(token) % 4
  96. if mod_returned:
  97. missing_padding = 4 - mod_returned
  98. token += '=' * missing_padding
  99. return token
  100. @classmethod
  101. def creation_time(cls, fernet_token):
  102. """Return the creation time of a valid Fernet token.
  103. :type fernet_token: six.text_type
  104. """
  105. fernet_token = TokenFormatter.restore_padding(fernet_token)
  106. # fernet_token is six.text_type
  107. # Fernet tokens are base64 encoded, so we need to unpack them first
  108. # urlsafe_b64decode() requires six.binary_type
  109. token_bytes = base64.urlsafe_b64decode(fernet_token.encode('utf-8'))
  110. # slice into the byte array to get just the timestamp
  111. timestamp_bytes = token_bytes[TIMESTAMP_START:TIMESTAMP_END]
  112. # convert those bytes to an integer
  113. # (it's a 64-bit "unsigned long long int" in C)
  114. timestamp_int = struct.unpack(">Q", timestamp_bytes)[0]
  115. # and with an integer, it's trivial to produce a datetime object
  116. issued_at = datetime.datetime.utcfromtimestamp(timestamp_int)
  117. return issued_at
  118. def create_token(self, user_id, expires_at, audit_ids, methods=None,
  119. domain_id=None, project_id=None, trust_id=None,
  120. federated_info=None, access_token_id=None):
  121. """Given a set of payload attributes, generate a Fernet token."""
  122. for payload_class in PAYLOAD_CLASSES:
  123. if payload_class.create_arguments_apply(
  124. project_id=project_id, domain_id=domain_id,
  125. trust_id=trust_id, federated_info=federated_info,
  126. access_token_id=access_token_id):
  127. break
  128. version = payload_class.version
  129. payload = payload_class.assemble(
  130. user_id, methods, project_id, domain_id, expires_at, audit_ids,
  131. trust_id, federated_info, access_token_id
  132. )
  133. versioned_payload = (version,) + payload
  134. serialized_payload = msgpack.packb(versioned_payload)
  135. token = self.pack(serialized_payload)
  136. # NOTE(lbragstad): We should warn against Fernet tokens that are over
  137. # 255 characters in length. This is mostly due to persisting the tokens
  138. # in a backend store of some kind that might have a limit of 255
  139. # characters. Even though Keystone isn't storing a Fernet token
  140. # anywhere, we can't say it isn't being stored somewhere else with
  141. # those kind of backend constraints.
  142. if len(token) > 255:
  143. LOG.info(_LI('Fernet token created with length of %d '
  144. 'characters, which exceeds 255 characters'),
  145. len(token))
  146. return token
  147. def validate_token(self, token):
  148. """Validate a Fernet token and returns the payload attributes.
  149. :type token: six.text_type
  150. """
  151. serialized_payload = self.unpack(token)
  152. versioned_payload = msgpack.unpackb(serialized_payload)
  153. version, payload = versioned_payload[0], versioned_payload[1:]
  154. for payload_class in PAYLOAD_CLASSES:
  155. if version == payload_class.version:
  156. (user_id, methods, project_id, domain_id, expires_at,
  157. audit_ids, trust_id, federated_info, access_token_id) = (
  158. payload_class.disassemble(payload))
  159. break
  160. else:
  161. # If the token_format is not recognized, raise ValidationError.
  162. raise exception.ValidationError(_(
  163. 'This is not a recognized Fernet payload version: %s') %
  164. version)
  165. # rather than appearing in the payload, the creation time is encoded
  166. # into the token format itself
  167. issued_at = TokenFormatter.creation_time(token)
  168. issued_at = ks_utils.isotime(at=issued_at, subsecond=True)
  169. expires_at = timeutils.parse_isotime(expires_at)
  170. expires_at = ks_utils.isotime(at=expires_at, subsecond=True)
  171. return (user_id, methods, audit_ids, domain_id, project_id, trust_id,
  172. federated_info, access_token_id, issued_at, expires_at)
  173. class BasePayload(object):
  174. # each payload variant should have a unique version
  175. version = None
  176. @classmethod
  177. def create_arguments_apply(cls, **kwargs):
  178. """Check the arguments to see if they apply to this payload variant.
  179. :returns: True if the arguments indicate that this payload class is
  180. needed for the token otherwise returns False.
  181. :rtype: bool
  182. """
  183. raise NotImplementedError()
  184. @classmethod
  185. def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
  186. audit_ids, trust_id, federated_info, access_token_id):
  187. """Assemble the payload of a token.
  188. :param user_id: identifier of the user in the token request
  189. :param methods: list of authentication methods used
  190. :param project_id: ID of the project to scope to
  191. :param domain_id: ID of the domain to scope to
  192. :param expires_at: datetime of the token's expiration
  193. :param audit_ids: list of the token's audit IDs
  194. :param trust_id: ID of the trust in effect
  195. :param federated_info: dictionary containing group IDs, the identity
  196. provider ID, protocol ID, and federated domain
  197. ID
  198. :param access_token_id: ID of the secret in OAuth1 authentication
  199. :returns: the payload of a token
  200. """
  201. raise NotImplementedError()
  202. @classmethod
  203. def disassemble(cls, payload):
  204. """Disassemble an unscoped payload into the component data.
  205. The tuple consists of::
  206. (user_id, methods, project_id, domain_id, expires_at_str,
  207. audit_ids, trust_id, federated_info, access_token_id)
  208. * ``methods`` are the auth methods.
  209. * federated_info is a dict contains the group IDs, the identity
  210. provider ID, the protocol ID, and the federated domain ID
  211. Fields will be set to None if they didn't apply to this payload type.
  212. :param payload: this variant of payload
  213. :returns: a tuple of the payloads component data
  214. """
  215. raise NotImplementedError()
  216. @classmethod
  217. def convert_uuid_hex_to_bytes(cls, uuid_string):
  218. """Compress UUID formatted strings to bytes.
  219. :param uuid_string: uuid string to compress to bytes
  220. :returns: a byte representation of the uuid
  221. """
  222. uuid_obj = uuid.UUID(uuid_string)
  223. return uuid_obj.bytes
  224. @classmethod
  225. def convert_uuid_bytes_to_hex(cls, uuid_byte_string):
  226. """Generate uuid.hex format based on byte string.
  227. :param uuid_byte_string: uuid string to generate from
  228. :returns: uuid hex formatted string
  229. """
  230. uuid_obj = uuid.UUID(bytes=uuid_byte_string)
  231. return uuid_obj.hex
  232. @classmethod
  233. def _convert_time_string_to_float(cls, time_string):
  234. """Convert a time formatted string to a float.
  235. :param time_string: time formatted string
  236. :returns: a timestamp as a float
  237. """
  238. time_object = timeutils.parse_isotime(time_string)
  239. return (timeutils.normalize_time(time_object) -
  240. datetime.datetime.utcfromtimestamp(0)).total_seconds()
  241. @classmethod
  242. def _convert_float_to_time_string(cls, time_float):
  243. """Convert a floating point timestamp to a string.
  244. :param time_float: integer representing timestamp
  245. :returns: a time formatted strings
  246. """
  247. time_object = datetime.datetime.utcfromtimestamp(time_float)
  248. return ks_utils.isotime(time_object, subsecond=True)
  249. @classmethod
  250. def attempt_convert_uuid_hex_to_bytes(cls, value):
  251. """Attempt to convert value to bytes or return value.
  252. :param value: value to attempt to convert to bytes
  253. :returns: tuple containing boolean indicating whether user_id was
  254. stored as bytes and uuid value as bytes or the original value
  255. """
  256. try:
  257. return (True, cls.convert_uuid_hex_to_bytes(value))
  258. except ValueError:
  259. # this might not be a UUID, depending on the situation (i.e.
  260. # federation)
  261. return (False, value)
  262. @classmethod
  263. def base64_encode(cls, s):
  264. """Encode a URL-safe string.
  265. :type s: six.text_type
  266. :rtype: six.text_type
  267. """
  268. # urlsafe_b64encode() returns six.binary_type so need to convert to
  269. # six.text_type, might as well do it before stripping.
  270. return base64.urlsafe_b64encode(s).decode('utf-8').rstrip('=')
  271. @classmethod
  272. def random_urlsafe_str_to_bytes(cls, s):
  273. """Convert a string from :func:`random_urlsafe_str()` to six.binary_type.
  274. :type s: six.text_type
  275. :rtype: six.binary_type
  276. """
  277. # urlsafe_b64decode() requires str, unicode isn't accepted.
  278. s = str(s)
  279. # restore the padding (==) at the end of the string
  280. return base64.urlsafe_b64decode(s + '==')
  281. class UnscopedPayload(BasePayload):
  282. version = 0
  283. @classmethod
  284. def create_arguments_apply(cls, **kwargs):
  285. return True
  286. @classmethod
  287. def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
  288. audit_ids, trust_id, federated_info, access_token_id):
  289. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  290. methods = auth_plugins.convert_method_list_to_integer(methods)
  291. expires_at_int = cls._convert_time_string_to_float(expires_at)
  292. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  293. audit_ids))
  294. return (b_user_id, methods, expires_at_int, b_audit_ids)
  295. @classmethod
  296. def disassemble(cls, payload):
  297. (is_stored_as_bytes, user_id) = payload[0]
  298. if is_stored_as_bytes:
  299. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  300. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  301. expires_at_str = cls._convert_float_to_time_string(payload[2])
  302. audit_ids = list(map(cls.base64_encode, payload[3]))
  303. project_id = None
  304. domain_id = None
  305. trust_id = None
  306. federated_info = None
  307. access_token_id = None
  308. return (user_id, methods, project_id, domain_id, expires_at_str,
  309. audit_ids, trust_id, federated_info, access_token_id)
  310. class DomainScopedPayload(BasePayload):
  311. version = 1
  312. @classmethod
  313. def create_arguments_apply(cls, **kwargs):
  314. return kwargs['domain_id']
  315. @classmethod
  316. def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
  317. audit_ids, trust_id, federated_info, access_token_id):
  318. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  319. methods = auth_plugins.convert_method_list_to_integer(methods)
  320. try:
  321. b_domain_id = cls.convert_uuid_hex_to_bytes(domain_id)
  322. except ValueError:
  323. # the default domain ID is configurable, and probably isn't a UUID
  324. if domain_id == CONF.identity.default_domain_id:
  325. b_domain_id = domain_id
  326. else:
  327. raise
  328. expires_at_int = cls._convert_time_string_to_float(expires_at)
  329. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  330. audit_ids))
  331. return (b_user_id, methods, b_domain_id, expires_at_int, b_audit_ids)
  332. @classmethod
  333. def disassemble(cls, payload):
  334. (is_stored_as_bytes, user_id) = payload[0]
  335. if is_stored_as_bytes:
  336. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  337. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  338. try:
  339. domain_id = cls.convert_uuid_bytes_to_hex(payload[2])
  340. except ValueError:
  341. # the default domain ID is configurable, and probably isn't a UUID
  342. if six.PY3 and isinstance(payload[2], six.binary_type):
  343. payload[2] = payload[2].decode('utf-8')
  344. if payload[2] == CONF.identity.default_domain_id:
  345. domain_id = payload[2]
  346. else:
  347. raise
  348. expires_at_str = cls._convert_float_to_time_string(payload[3])
  349. audit_ids = list(map(cls.base64_encode, payload[4]))
  350. project_id = None
  351. trust_id = None
  352. federated_info = None
  353. access_token_id = None
  354. return (user_id, methods, project_id, domain_id, expires_at_str,
  355. audit_ids, trust_id, federated_info, access_token_id)
  356. class ProjectScopedPayload(BasePayload):
  357. version = 2
  358. @classmethod
  359. def create_arguments_apply(cls, **kwargs):
  360. return kwargs['project_id']
  361. @classmethod
  362. def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
  363. audit_ids, trust_id, federated_info, access_token_id):
  364. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  365. methods = auth_plugins.convert_method_list_to_integer(methods)
  366. b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
  367. expires_at_int = cls._convert_time_string_to_float(expires_at)
  368. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  369. audit_ids))
  370. return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids)
  371. @classmethod
  372. def disassemble(cls, payload):
  373. (is_stored_as_bytes, user_id) = payload[0]
  374. if is_stored_as_bytes:
  375. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  376. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  377. (is_stored_as_bytes, project_id) = payload[2]
  378. if is_stored_as_bytes:
  379. project_id = cls.convert_uuid_bytes_to_hex(project_id)
  380. expires_at_str = cls._convert_float_to_time_string(payload[3])
  381. audit_ids = list(map(cls.base64_encode, payload[4]))
  382. domain_id = None
  383. trust_id = None
  384. federated_info = None
  385. access_token_id = None
  386. return (user_id, methods, project_id, domain_id, expires_at_str,
  387. audit_ids, trust_id, federated_info, access_token_id)
  388. class TrustScopedPayload(BasePayload):
  389. version = 3
  390. @classmethod
  391. def create_arguments_apply(cls, **kwargs):
  392. return kwargs['trust_id']
  393. @classmethod
  394. def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
  395. audit_ids, trust_id, federated_info, access_token_id):
  396. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  397. methods = auth_plugins.convert_method_list_to_integer(methods)
  398. b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
  399. b_trust_id = cls.convert_uuid_hex_to_bytes(trust_id)
  400. expires_at_int = cls._convert_time_string_to_float(expires_at)
  401. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  402. audit_ids))
  403. return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids,
  404. b_trust_id)
  405. @classmethod
  406. def disassemble(cls, payload):
  407. (is_stored_as_bytes, user_id) = payload[0]
  408. if is_stored_as_bytes:
  409. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  410. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  411. (is_stored_as_bytes, project_id) = payload[2]
  412. if is_stored_as_bytes:
  413. project_id = cls.convert_uuid_bytes_to_hex(project_id)
  414. expires_at_str = cls._convert_float_to_time_string(payload[3])
  415. audit_ids = list(map(cls.base64_encode, payload[4]))
  416. trust_id = cls.convert_uuid_bytes_to_hex(payload[5])
  417. domain_id = None
  418. federated_info = None
  419. access_token_id = None
  420. return (user_id, methods, project_id, domain_id, expires_at_str,
  421. audit_ids, trust_id, federated_info, access_token_id)
  422. class FederatedUnscopedPayload(BasePayload):
  423. version = 4
  424. @classmethod
  425. def create_arguments_apply(cls, **kwargs):
  426. return kwargs['federated_info']
  427. @classmethod
  428. def pack_group_id(cls, group_dict):
  429. return cls.attempt_convert_uuid_hex_to_bytes(group_dict['id'])
  430. @classmethod
  431. def unpack_group_id(cls, group_id_in_bytes):
  432. (is_stored_as_bytes, group_id) = group_id_in_bytes
  433. if is_stored_as_bytes:
  434. group_id = cls.convert_uuid_bytes_to_hex(group_id)
  435. return {'id': group_id}
  436. @classmethod
  437. def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
  438. audit_ids, trust_id, federated_info, access_token_id):
  439. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  440. methods = auth_plugins.convert_method_list_to_integer(methods)
  441. b_group_ids = list(map(cls.pack_group_id,
  442. federated_info['group_ids']))
  443. b_idp_id = cls.attempt_convert_uuid_hex_to_bytes(
  444. federated_info['idp_id'])
  445. protocol_id = federated_info['protocol_id']
  446. expires_at_int = cls._convert_time_string_to_float(expires_at)
  447. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  448. audit_ids))
  449. return (b_user_id, methods, b_group_ids, b_idp_id, protocol_id,
  450. expires_at_int, b_audit_ids)
  451. @classmethod
  452. def disassemble(cls, payload):
  453. (is_stored_as_bytes, user_id) = payload[0]
  454. if is_stored_as_bytes:
  455. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  456. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  457. group_ids = list(map(cls.unpack_group_id, payload[2]))
  458. (is_stored_as_bytes, idp_id) = payload[3]
  459. if is_stored_as_bytes:
  460. idp_id = cls.convert_uuid_bytes_to_hex(idp_id)
  461. else:
  462. idp_id = idp_id.decode('utf-8')
  463. protocol_id = payload[4]
  464. if isinstance(protocol_id, six.binary_type):
  465. protocol_id = protocol_id.decode('utf-8')
  466. expires_at_str = cls._convert_float_to_time_string(payload[5])
  467. audit_ids = list(map(cls.base64_encode, payload[6]))
  468. federated_info = dict(group_ids=group_ids, idp_id=idp_id,
  469. protocol_id=protocol_id)
  470. project_id = None
  471. domain_id = None
  472. trust_id = None
  473. access_token_id = None
  474. return (user_id, methods, project_id, domain_id, expires_at_str,
  475. audit_ids, trust_id, federated_info, access_token_id)
  476. class FederatedScopedPayload(FederatedUnscopedPayload):
  477. version = None
  478. @classmethod
  479. def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
  480. audit_ids, trust_id, federated_info, access_token_id):
  481. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  482. methods = auth_plugins.convert_method_list_to_integer(methods)
  483. b_scope_id = cls.attempt_convert_uuid_hex_to_bytes(
  484. project_id or domain_id)
  485. b_group_ids = list(map(cls.pack_group_id,
  486. federated_info['group_ids']))
  487. b_idp_id = cls.attempt_convert_uuid_hex_to_bytes(
  488. federated_info['idp_id'])
  489. protocol_id = federated_info['protocol_id']
  490. expires_at_int = cls._convert_time_string_to_float(expires_at)
  491. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  492. audit_ids))
  493. return (b_user_id, methods, b_scope_id, b_group_ids, b_idp_id,
  494. protocol_id, expires_at_int, b_audit_ids)
  495. @classmethod
  496. def disassemble(cls, payload):
  497. (is_stored_as_bytes, user_id) = payload[0]
  498. if is_stored_as_bytes:
  499. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  500. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  501. (is_stored_as_bytes, scope_id) = payload[2]
  502. if is_stored_as_bytes:
  503. scope_id = cls.convert_uuid_bytes_to_hex(scope_id)
  504. project_id = (
  505. scope_id
  506. if cls.version == FederatedProjectScopedPayload.version else None)
  507. domain_id = (
  508. scope_id
  509. if cls.version == FederatedDomainScopedPayload.version else None)
  510. group_ids = list(map(cls.unpack_group_id, payload[3]))
  511. (is_stored_as_bytes, idp_id) = payload[4]
  512. if is_stored_as_bytes:
  513. idp_id = cls.convert_uuid_bytes_to_hex(idp_id)
  514. protocol_id = payload[5]
  515. expires_at_str = cls._convert_float_to_time_string(payload[6])
  516. audit_ids = list(map(cls.base64_encode, payload[7]))
  517. federated_info = dict(idp_id=idp_id, protocol_id=protocol_id,
  518. group_ids=group_ids)
  519. trust_id = None
  520. access_token_id = None
  521. return (user_id, methods, project_id, domain_id, expires_at_str,
  522. audit_ids, trust_id, federated_info, access_token_id)
  523. class FederatedProjectScopedPayload(FederatedScopedPayload):
  524. version = 5
  525. @classmethod
  526. def create_arguments_apply(cls, **kwargs):
  527. return kwargs['project_id'] and kwargs['federated_info']
  528. class FederatedDomainScopedPayload(FederatedScopedPayload):
  529. version = 6
  530. @classmethod
  531. def create_arguments_apply(cls, **kwargs):
  532. return kwargs['domain_id'] and kwargs['federated_info']
  533. class OauthScopedPayload(BasePayload):
  534. version = 7
  535. @classmethod
  536. def create_arguments_apply(cls, **kwargs):
  537. return kwargs['access_token_id']
  538. @classmethod
  539. def assemble(cls, user_id, methods, project_id, domain_id, expires_at,
  540. audit_ids, trust_id, federated_info, access_token_id):
  541. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  542. methods = auth_plugins.convert_method_list_to_integer(methods)
  543. b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
  544. expires_at_int = cls._convert_time_string_to_float(expires_at)
  545. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  546. audit_ids))
  547. b_access_token_id = cls.attempt_convert_uuid_hex_to_bytes(
  548. access_token_id)
  549. return (b_user_id, methods, b_project_id, b_access_token_id,
  550. expires_at_int, b_audit_ids)
  551. @classmethod
  552. def disassemble(cls, payload):
  553. (is_stored_as_bytes, user_id) = payload[0]
  554. if is_stored_as_bytes:
  555. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  556. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  557. (is_stored_as_bytes, project_id) = payload[2]
  558. if is_stored_as_bytes:
  559. project_id = cls.convert_uuid_bytes_to_hex(project_id)
  560. (is_stored_as_bytes, access_token_id) = payload[3]
  561. if is_stored_as_bytes:
  562. access_token_id = cls.convert_uuid_bytes_to_hex(access_token_id)
  563. expires_at_str = cls._convert_float_to_time_string(payload[4])
  564. audit_ids = list(map(cls.base64_encode, payload[5]))
  565. domain_id = None
  566. trust_id = None
  567. federated_info = None
  568. return (user_id, methods, project_id, domain_id, expires_at_str,
  569. audit_ids, trust_id, federated_info, access_token_id)
  570. # For now, the order of the classes in the following list is important. This
  571. # is because the way they test that the payload applies to them in
  572. # the create_arguments_apply method requires that the previous ones rejected
  573. # the payload arguments. For example, UnscopedPayload must be last since it's
  574. # the catch-all after all the other payloads have been checked.
  575. # TODO(blk-u): Clean up the create_arguments_apply methods so that they don't
  576. # depend on the previous classes then these can be in any order.
  577. PAYLOAD_CLASSES = [
  578. OauthScopedPayload,
  579. TrustScopedPayload,
  580. FederatedProjectScopedPayload,
  581. FederatedDomainScopedPayload,
  582. FederatedUnscopedPayload,
  583. ProjectScopedPayload,
  584. DomainScopedPayload,
  585. UnscopedPayload,
  586. ]