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.

token_formatters.py 32KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  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 keystone.auth import plugins as auth_plugins
  23. from keystone.common import fernet_utils as utils
  24. from keystone.common import utils as ks_utils
  25. import keystone.conf
  26. from keystone import exception
  27. from keystone.i18n import _
  28. CONF = keystone.conf.CONF
  29. LOG = log.getLogger(__name__)
  30. # Fernet byte indexes as computed by pypi/keyless_fernet and defined in
  31. # https://github.com/fernet/spec
  32. TIMESTAMP_START = 1
  33. TIMESTAMP_END = 9
  34. class TokenFormatter(object):
  35. """Packs and unpacks payloads into tokens for transport."""
  36. @property
  37. def crypto(self):
  38. """Return a cryptography instance.
  39. You can extend this class with a custom crypto @property to provide
  40. your own token encoding / decoding. For example, using a different
  41. cryptography library (e.g. ``python-keyczar``) or to meet arbitrary
  42. security requirements.
  43. This @property just needs to return an object that implements
  44. ``encrypt(plaintext)`` and ``decrypt(ciphertext)``.
  45. """
  46. fernet_utils = utils.FernetUtils(
  47. CONF.fernet_tokens.key_repository,
  48. CONF.fernet_tokens.max_active_keys,
  49. 'fernet_tokens'
  50. )
  51. keys = fernet_utils.load_keys()
  52. if not keys:
  53. raise exception.KeysNotFound()
  54. fernet_instances = [fernet.Fernet(key) for key in keys]
  55. return fernet.MultiFernet(fernet_instances)
  56. def pack(self, payload):
  57. """Pack a payload for transport as a token.
  58. :type payload: six.binary_type
  59. :rtype: six.text_type
  60. """
  61. # base64 padding (if any) is not URL-safe
  62. return self.crypto.encrypt(payload).rstrip(b'=').decode('utf-8')
  63. def unpack(self, token):
  64. """Unpack a token, and validate the payload.
  65. :type token: six.text_type
  66. :rtype: six.binary_type
  67. """
  68. token = TokenFormatter.restore_padding(token)
  69. try:
  70. return self.crypto.decrypt(token.encode('utf-8'))
  71. except fernet.InvalidToken:
  72. raise exception.ValidationError(
  73. _('Could not recognize Fernet token'))
  74. @classmethod
  75. def restore_padding(cls, token):
  76. """Restore padding based on token size.
  77. :param token: token to restore padding on
  78. :type token: six.text_type
  79. :returns: token with correct padding
  80. """
  81. # Re-inflate the padding
  82. mod_returned = len(token) % 4
  83. if mod_returned:
  84. missing_padding = 4 - mod_returned
  85. token += '=' * missing_padding
  86. return token
  87. @classmethod
  88. def creation_time(cls, fernet_token):
  89. """Return the creation time of a valid Fernet token.
  90. :type fernet_token: six.text_type
  91. """
  92. fernet_token = TokenFormatter.restore_padding(fernet_token)
  93. # fernet_token is six.text_type
  94. # Fernet tokens are base64 encoded, so we need to unpack them first
  95. # urlsafe_b64decode() requires six.binary_type
  96. token_bytes = base64.urlsafe_b64decode(fernet_token.encode('utf-8'))
  97. # slice into the byte array to get just the timestamp
  98. timestamp_bytes = token_bytes[TIMESTAMP_START:TIMESTAMP_END]
  99. # convert those bytes to an integer
  100. # (it's a 64-bit "unsigned long long int" in C)
  101. timestamp_int = struct.unpack(">Q", timestamp_bytes)[0]
  102. # and with an integer, it's trivial to produce a datetime object
  103. issued_at = datetime.datetime.utcfromtimestamp(timestamp_int)
  104. return issued_at
  105. def create_token(self, user_id, expires_at, audit_ids, payload_class,
  106. methods=None, system=None, domain_id=None,
  107. project_id=None, trust_id=None, federated_group_ids=None,
  108. identity_provider_id=None, protocol_id=None,
  109. access_token_id=None, app_cred_id=None):
  110. """Given a set of payload attributes, generate a Fernet token."""
  111. version = payload_class.version
  112. payload = payload_class.assemble(
  113. user_id, methods, system, project_id, domain_id, expires_at,
  114. audit_ids, trust_id, federated_group_ids, identity_provider_id,
  115. protocol_id, access_token_id, app_cred_id
  116. )
  117. versioned_payload = (version,) + payload
  118. serialized_payload = msgpack.packb(versioned_payload)
  119. token = self.pack(serialized_payload)
  120. # NOTE(lbragstad): We should warn against Fernet tokens that are over
  121. # 255 characters in length. This is mostly due to persisting the tokens
  122. # in a backend store of some kind that might have a limit of 255
  123. # characters. Even though Keystone isn't storing a Fernet token
  124. # anywhere, we can't say it isn't being stored somewhere else with
  125. # those kind of backend constraints.
  126. if len(token) > 255:
  127. LOG.info('Fernet token created with length of %d '
  128. 'characters, which exceeds 255 characters',
  129. len(token))
  130. return token
  131. def validate_token(self, token):
  132. """Validate a Fernet token and returns the payload attributes.
  133. :type token: six.text_type
  134. """
  135. serialized_payload = self.unpack(token)
  136. versioned_payload = msgpack.unpackb(serialized_payload)
  137. version, payload = versioned_payload[0], versioned_payload[1:]
  138. for payload_class in _PAYLOAD_CLASSES:
  139. if version == payload_class.version:
  140. (user_id, methods, system, project_id, domain_id,
  141. expires_at, audit_ids, trust_id, federated_group_ids,
  142. identity_provider_id, protocol_id, access_token_id,
  143. app_cred_id) = payload_class.disassemble(payload)
  144. break
  145. else:
  146. # If the token_format is not recognized, raise ValidationError.
  147. raise exception.ValidationError(_(
  148. 'This is not a recognized Fernet payload version: %s') %
  149. version)
  150. # FIXME(lbragstad): Without this, certain token validation tests fail
  151. # when running with python 3. Once we get further along in this
  152. # refactor, we should be better about handling string encoding/types at
  153. # the edges of the application.
  154. if isinstance(system, bytes):
  155. system = system.decode('utf-8')
  156. # rather than appearing in the payload, the creation time is encoded
  157. # into the token format itself
  158. issued_at = TokenFormatter.creation_time(token)
  159. issued_at = ks_utils.isotime(at=issued_at, subsecond=True)
  160. expires_at = timeutils.parse_isotime(expires_at)
  161. expires_at = ks_utils.isotime(at=expires_at, subsecond=True)
  162. return (user_id, methods, audit_ids, system, domain_id, project_id,
  163. trust_id, federated_group_ids, identity_provider_id,
  164. protocol_id, access_token_id, app_cred_id, issued_at,
  165. expires_at)
  166. class BasePayload(object):
  167. # each payload variant should have a unique version
  168. version = None
  169. @classmethod
  170. def assemble(cls, user_id, methods, system, project_id, domain_id,
  171. expires_at, audit_ids, trust_id, federated_group_ids,
  172. identity_provider_id, protocol_id, access_token_id,
  173. app_cred_id):
  174. """Assemble the payload of a token.
  175. :param user_id: identifier of the user in the token request
  176. :param methods: list of authentication methods used
  177. :param system: a string including system scope information
  178. :param project_id: ID of the project to scope to
  179. :param domain_id: ID of the domain to scope to
  180. :param expires_at: datetime of the token's expiration
  181. :param audit_ids: list of the token's audit IDs
  182. :param trust_id: ID of the trust in effect
  183. :param federated_group_ids: list of group IDs from SAML assertion
  184. :param identity_provider_id: ID of the user's identity provider
  185. :param protocol_id: federated protocol used for authentication
  186. :param access_token_id: ID of the secret in OAuth1 authentication
  187. :param app_cred_id: ID of the application credential in effect
  188. :returns: the payload of a token
  189. """
  190. raise NotImplementedError()
  191. @classmethod
  192. def disassemble(cls, payload):
  193. """Disassemble an unscoped payload into the component data.
  194. The tuple consists of::
  195. (user_id, methods, system, project_id, domain_id,
  196. expires_at_str, audit_ids, trust_id, federated_group_ids,
  197. identity_provider_id, protocol_id,` access_token_id, app_cred_id)
  198. * ``methods`` are the auth methods.
  199. Fields will be set to None if they didn't apply to this payload type.
  200. :param payload: this variant of payload
  201. :returns: a tuple of the payloads component data
  202. """
  203. raise NotImplementedError()
  204. @classmethod
  205. def convert_uuid_hex_to_bytes(cls, uuid_string):
  206. """Compress UUID formatted strings to bytes.
  207. :param uuid_string: uuid string to compress to bytes
  208. :returns: a byte representation of the uuid
  209. """
  210. uuid_obj = uuid.UUID(uuid_string)
  211. return uuid_obj.bytes
  212. @classmethod
  213. def convert_uuid_bytes_to_hex(cls, uuid_byte_string):
  214. """Generate uuid.hex format based on byte string.
  215. :param uuid_byte_string: uuid string to generate from
  216. :returns: uuid hex formatted string
  217. """
  218. uuid_obj = uuid.UUID(bytes=uuid_byte_string)
  219. return uuid_obj.hex
  220. @classmethod
  221. def _convert_time_string_to_float(cls, time_string):
  222. """Convert a time formatted string to a float.
  223. :param time_string: time formatted string
  224. :returns: a timestamp as a float
  225. """
  226. time_object = timeutils.parse_isotime(time_string)
  227. return (timeutils.normalize_time(time_object) -
  228. datetime.datetime.utcfromtimestamp(0)).total_seconds()
  229. @classmethod
  230. def _convert_float_to_time_string(cls, time_float):
  231. """Convert a floating point timestamp to a string.
  232. :param time_float: integer representing timestamp
  233. :returns: a time formatted strings
  234. """
  235. time_object = datetime.datetime.utcfromtimestamp(time_float)
  236. return ks_utils.isotime(time_object, subsecond=True)
  237. @classmethod
  238. def attempt_convert_uuid_hex_to_bytes(cls, value):
  239. """Attempt to convert value to bytes or return value.
  240. :param value: value to attempt to convert to bytes
  241. :returns: tuple containing boolean indicating whether user_id was
  242. stored as bytes and uuid value as bytes or the original value
  243. """
  244. try:
  245. return (True, cls.convert_uuid_hex_to_bytes(value))
  246. except ValueError:
  247. # this might not be a UUID, depending on the situation (i.e.
  248. # federation)
  249. return (False, value)
  250. @classmethod
  251. def base64_encode(cls, s):
  252. """Encode a URL-safe string.
  253. :type s: six.text_type
  254. :rtype: six.text_type
  255. """
  256. # urlsafe_b64encode() returns six.binary_type so need to convert to
  257. # six.text_type, might as well do it before stripping.
  258. return base64.urlsafe_b64encode(s).decode('utf-8').rstrip('=')
  259. @classmethod
  260. def random_urlsafe_str_to_bytes(cls, s):
  261. """Convert a string from :func:`random_urlsafe_str()` to six.binary_type.
  262. :type s: six.text_type
  263. :rtype: six.binary_type
  264. """
  265. # urlsafe_b64decode() requires str, unicode isn't accepted.
  266. s = str(s)
  267. # restore the padding (==) at the end of the string
  268. return base64.urlsafe_b64decode(s + '==')
  269. class UnscopedPayload(BasePayload):
  270. version = 0
  271. @classmethod
  272. def assemble(cls, user_id, methods, system, project_id, domain_id,
  273. expires_at, audit_ids, trust_id, federated_group_ids,
  274. identity_provider_id, protocol_id, access_token_id,
  275. app_cred_id):
  276. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  277. methods = auth_plugins.convert_method_list_to_integer(methods)
  278. expires_at_int = cls._convert_time_string_to_float(expires_at)
  279. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  280. audit_ids))
  281. return (b_user_id, methods, expires_at_int, b_audit_ids)
  282. @classmethod
  283. def disassemble(cls, payload):
  284. (is_stored_as_bytes, user_id) = payload[0]
  285. if is_stored_as_bytes:
  286. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  287. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  288. expires_at_str = cls._convert_float_to_time_string(payload[2])
  289. audit_ids = list(map(cls.base64_encode, payload[3]))
  290. system = None
  291. project_id = None
  292. domain_id = None
  293. trust_id = None
  294. federated_group_ids = None
  295. identity_provider_id = None
  296. protocol_id = None
  297. access_token_id = None
  298. app_cred_id = None
  299. return (user_id, methods, system, project_id, domain_id,
  300. expires_at_str, audit_ids, trust_id, federated_group_ids,
  301. identity_provider_id, protocol_id, access_token_id,
  302. app_cred_id)
  303. class DomainScopedPayload(BasePayload):
  304. version = 1
  305. @classmethod
  306. def assemble(cls, user_id, methods, system, project_id, domain_id,
  307. expires_at, audit_ids, trust_id, federated_group_ids,
  308. identity_provider_id, protocol_id, access_token_id,
  309. app_cred_id):
  310. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  311. methods = auth_plugins.convert_method_list_to_integer(methods)
  312. try:
  313. b_domain_id = cls.convert_uuid_hex_to_bytes(domain_id)
  314. except ValueError:
  315. # the default domain ID is configurable, and probably isn't a UUID
  316. if domain_id == CONF.identity.default_domain_id:
  317. b_domain_id = domain_id
  318. else:
  319. raise
  320. expires_at_int = cls._convert_time_string_to_float(expires_at)
  321. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  322. audit_ids))
  323. return (b_user_id, methods, b_domain_id, expires_at_int, b_audit_ids)
  324. @classmethod
  325. def disassemble(cls, payload):
  326. (is_stored_as_bytes, user_id) = payload[0]
  327. if is_stored_as_bytes:
  328. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  329. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  330. try:
  331. domain_id = cls.convert_uuid_bytes_to_hex(payload[2])
  332. except ValueError:
  333. # the default domain ID is configurable, and probably isn't a UUID
  334. if six.PY3 and isinstance(payload[2], six.binary_type):
  335. payload[2] = payload[2].decode('utf-8')
  336. if payload[2] == CONF.identity.default_domain_id:
  337. domain_id = payload[2]
  338. else:
  339. raise
  340. expires_at_str = cls._convert_float_to_time_string(payload[3])
  341. audit_ids = list(map(cls.base64_encode, payload[4]))
  342. system = None
  343. project_id = None
  344. trust_id = None
  345. federated_group_ids = None
  346. identity_provider_id = None
  347. protocol_id = None
  348. access_token_id = None
  349. app_cred_id = None
  350. return (user_id, methods, system, project_id, domain_id,
  351. expires_at_str, audit_ids, trust_id, federated_group_ids,
  352. identity_provider_id, protocol_id, access_token_id,
  353. app_cred_id)
  354. class ProjectScopedPayload(BasePayload):
  355. version = 2
  356. @classmethod
  357. def assemble(cls, user_id, methods, system, project_id, domain_id,
  358. expires_at, audit_ids, trust_id, federated_group_ids,
  359. identity_provider_id, protocol_id, access_token_id,
  360. app_cred_id):
  361. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  362. methods = auth_plugins.convert_method_list_to_integer(methods)
  363. b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
  364. expires_at_int = cls._convert_time_string_to_float(expires_at)
  365. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  366. audit_ids))
  367. return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids)
  368. @classmethod
  369. def disassemble(cls, payload):
  370. (is_stored_as_bytes, user_id) = payload[0]
  371. if is_stored_as_bytes:
  372. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  373. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  374. (is_stored_as_bytes, project_id) = payload[2]
  375. if is_stored_as_bytes:
  376. project_id = cls.convert_uuid_bytes_to_hex(project_id)
  377. expires_at_str = cls._convert_float_to_time_string(payload[3])
  378. audit_ids = list(map(cls.base64_encode, payload[4]))
  379. system = None
  380. domain_id = None
  381. trust_id = None
  382. federated_group_ids = None
  383. identity_provider_id = None
  384. protocol_id = None
  385. access_token_id = None
  386. app_cred_id = None
  387. return (user_id, methods, system, project_id, domain_id,
  388. expires_at_str, audit_ids, trust_id, federated_group_ids,
  389. identity_provider_id, protocol_id, access_token_id,
  390. app_cred_id)
  391. class TrustScopedPayload(BasePayload):
  392. version = 3
  393. @classmethod
  394. def assemble(cls, user_id, methods, system, project_id, domain_id,
  395. expires_at, audit_ids, trust_id, federated_group_ids,
  396. identity_provider_id, protocol_id, access_token_id,
  397. app_cred_id):
  398. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  399. methods = auth_plugins.convert_method_list_to_integer(methods)
  400. b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
  401. b_trust_id = cls.convert_uuid_hex_to_bytes(trust_id)
  402. expires_at_int = cls._convert_time_string_to_float(expires_at)
  403. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  404. audit_ids))
  405. return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids,
  406. b_trust_id)
  407. @classmethod
  408. def disassemble(cls, payload):
  409. (is_stored_as_bytes, user_id) = payload[0]
  410. if is_stored_as_bytes:
  411. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  412. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  413. (is_stored_as_bytes, project_id) = payload[2]
  414. if is_stored_as_bytes:
  415. project_id = cls.convert_uuid_bytes_to_hex(project_id)
  416. expires_at_str = cls._convert_float_to_time_string(payload[3])
  417. audit_ids = list(map(cls.base64_encode, payload[4]))
  418. trust_id = cls.convert_uuid_bytes_to_hex(payload[5])
  419. system = None
  420. domain_id = None
  421. federated_group_ids = None
  422. identity_provider_id = None
  423. protocol_id = None
  424. access_token_id = None
  425. app_cred_id = None
  426. return (user_id, methods, system, project_id, domain_id,
  427. expires_at_str, audit_ids, trust_id, federated_group_ids,
  428. identity_provider_id, protocol_id, access_token_id,
  429. app_cred_id)
  430. class FederatedUnscopedPayload(BasePayload):
  431. version = 4
  432. @classmethod
  433. def pack_group_id(cls, group_dict):
  434. return cls.attempt_convert_uuid_hex_to_bytes(group_dict['id'])
  435. @classmethod
  436. def unpack_group_id(cls, group_id_in_bytes):
  437. (is_stored_as_bytes, group_id) = group_id_in_bytes
  438. if is_stored_as_bytes:
  439. group_id = cls.convert_uuid_bytes_to_hex(group_id)
  440. return {'id': group_id}
  441. @classmethod
  442. def assemble(cls, user_id, methods, system, project_id, domain_id,
  443. expires_at, audit_ids, trust_id, federated_group_ids,
  444. identity_provider_id, protocol_id, access_token_id,
  445. app_cred_id):
  446. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  447. methods = auth_plugins.convert_method_list_to_integer(methods)
  448. b_group_ids = list(map(cls.pack_group_id, federated_group_ids))
  449. b_idp_id = cls.attempt_convert_uuid_hex_to_bytes(identity_provider_id)
  450. expires_at_int = cls._convert_time_string_to_float(expires_at)
  451. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  452. audit_ids))
  453. return (b_user_id, methods, b_group_ids, b_idp_id, protocol_id,
  454. expires_at_int, b_audit_ids)
  455. @classmethod
  456. def disassemble(cls, payload):
  457. (is_stored_as_bytes, user_id) = payload[0]
  458. if is_stored_as_bytes:
  459. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  460. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  461. group_ids = list(map(cls.unpack_group_id, payload[2]))
  462. (is_stored_as_bytes, idp_id) = payload[3]
  463. if is_stored_as_bytes:
  464. idp_id = cls.convert_uuid_bytes_to_hex(idp_id)
  465. else:
  466. idp_id = idp_id.decode('utf-8')
  467. protocol_id = payload[4]
  468. if isinstance(protocol_id, six.binary_type):
  469. protocol_id = protocol_id.decode('utf-8')
  470. expires_at_str = cls._convert_float_to_time_string(payload[5])
  471. audit_ids = list(map(cls.base64_encode, payload[6]))
  472. system = None
  473. project_id = None
  474. domain_id = None
  475. trust_id = None
  476. access_token_id = None
  477. app_cred_id = None
  478. return (user_id, methods, system, project_id, domain_id,
  479. expires_at_str, audit_ids, trust_id, group_ids, idp_id,
  480. protocol_id, access_token_id, app_cred_id)
  481. class FederatedScopedPayload(FederatedUnscopedPayload):
  482. version = None
  483. @classmethod
  484. def assemble(cls, user_id, methods, system, project_id, domain_id,
  485. expires_at, audit_ids, trust_id, federated_group_ids,
  486. identity_provider_id, protocol_id, access_token_id,
  487. app_cred_id):
  488. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  489. methods = auth_plugins.convert_method_list_to_integer(methods)
  490. b_scope_id = cls.attempt_convert_uuid_hex_to_bytes(
  491. project_id or domain_id)
  492. b_group_ids = list(map(cls.pack_group_id, federated_group_ids))
  493. b_idp_id = cls.attempt_convert_uuid_hex_to_bytes(identity_provider_id)
  494. expires_at_int = cls._convert_time_string_to_float(expires_at)
  495. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  496. audit_ids))
  497. return (b_user_id, methods, b_scope_id, b_group_ids, b_idp_id,
  498. protocol_id, expires_at_int, b_audit_ids)
  499. @classmethod
  500. def disassemble(cls, payload):
  501. (is_stored_as_bytes, user_id) = payload[0]
  502. if is_stored_as_bytes:
  503. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  504. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  505. (is_stored_as_bytes, scope_id) = payload[2]
  506. if is_stored_as_bytes:
  507. scope_id = cls.convert_uuid_bytes_to_hex(scope_id)
  508. else:
  509. # NOTE(lbragstad): We assembled the token payload scope as a tuple
  510. # (False, domain_id) for cases like (False, 'default'), since the
  511. # default domain ID isn't converted to a byte string when it's not
  512. # in UUID format. Despite the boolean indicator in the tuple that
  513. # denotes if the value is stored as a byte string or not, msgpack
  514. # apparently returns the serialized input as byte strings anyway.
  515. # For example, this means what we though we were passing in as
  516. # (False, 'default') during token creation actually comes out as
  517. # (False, b'default') in token validation through msgpack, which
  518. # clearly isn't correct according to our boolean indicator. This
  519. # causes comparison issues due to different string types (e.g.,
  520. # b'default' != 'default') with python 3. See bug 1813085 for
  521. # details. We use this pattern for other strings in the payload
  522. # like idp_id and protocol_id for the same reason.
  523. if six.PY3 and isinstance(scope_id, six.binary_type):
  524. scope_id = scope_id.decode('utf-8')
  525. project_id = (
  526. scope_id
  527. if cls.version == FederatedProjectScopedPayload.version else None)
  528. domain_id = (
  529. scope_id
  530. if cls.version == FederatedDomainScopedPayload.version else None)
  531. group_ids = list(map(cls.unpack_group_id, payload[3]))
  532. (is_stored_as_bytes, idp_id) = payload[4]
  533. if is_stored_as_bytes:
  534. idp_id = cls.convert_uuid_bytes_to_hex(idp_id)
  535. else:
  536. if six.PY3 and isinstance(idp_id, six.binary_type):
  537. idp_id = idp_id.decode('utf-8')
  538. protocol_id = payload[5]
  539. if six.PY3 and isinstance(protocol_id, six.binary_type):
  540. protocol_id = protocol_id.decode('utf-8')
  541. expires_at_str = cls._convert_float_to_time_string(payload[6])
  542. audit_ids = list(map(cls.base64_encode, payload[7]))
  543. system = None
  544. trust_id = None
  545. access_token_id = None
  546. app_cred_id = None
  547. return (user_id, methods, system, project_id, domain_id,
  548. expires_at_str, audit_ids, trust_id, group_ids, idp_id,
  549. protocol_id, access_token_id, app_cred_id)
  550. class FederatedProjectScopedPayload(FederatedScopedPayload):
  551. version = 5
  552. class FederatedDomainScopedPayload(FederatedScopedPayload):
  553. version = 6
  554. class OauthScopedPayload(BasePayload):
  555. version = 7
  556. @classmethod
  557. def assemble(cls, user_id, methods, system, project_id, domain_id,
  558. expires_at, audit_ids, trust_id, federated_group_ids,
  559. identity_provider_id, protocol_id, access_token_id,
  560. app_cred_id):
  561. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  562. methods = auth_plugins.convert_method_list_to_integer(methods)
  563. b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
  564. expires_at_int = cls._convert_time_string_to_float(expires_at)
  565. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  566. audit_ids))
  567. b_access_token_id = cls.attempt_convert_uuid_hex_to_bytes(
  568. access_token_id)
  569. return (b_user_id, methods, b_project_id, b_access_token_id,
  570. expires_at_int, b_audit_ids)
  571. @classmethod
  572. def disassemble(cls, payload):
  573. (is_stored_as_bytes, user_id) = payload[0]
  574. if is_stored_as_bytes:
  575. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  576. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  577. (is_stored_as_bytes, project_id) = payload[2]
  578. if is_stored_as_bytes:
  579. project_id = cls.convert_uuid_bytes_to_hex(project_id)
  580. (is_stored_as_bytes, access_token_id) = payload[3]
  581. if is_stored_as_bytes:
  582. access_token_id = cls.convert_uuid_bytes_to_hex(access_token_id)
  583. expires_at_str = cls._convert_float_to_time_string(payload[4])
  584. audit_ids = list(map(cls.base64_encode, payload[5]))
  585. system = None
  586. domain_id = None
  587. trust_id = None
  588. federated_group_ids = None
  589. identity_provider_id = None
  590. protocol_id = None
  591. app_cred_id = None
  592. return (user_id, methods, system, project_id, domain_id,
  593. expires_at_str, audit_ids, trust_id, federated_group_ids,
  594. identity_provider_id, protocol_id, access_token_id,
  595. app_cred_id)
  596. class SystemScopedPayload(BasePayload):
  597. version = 8
  598. @classmethod
  599. def assemble(cls, user_id, methods, system, project_id, domain_id,
  600. expires_at, audit_ids, trust_id, federated_group_ids,
  601. identity_provider_id, protocol_id, access_token_id,
  602. app_cred_id):
  603. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  604. methods = auth_plugins.convert_method_list_to_integer(methods)
  605. expires_at_int = cls._convert_time_string_to_float(expires_at)
  606. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  607. audit_ids))
  608. return (b_user_id, methods, system, expires_at_int, b_audit_ids)
  609. @classmethod
  610. def disassemble(cls, payload):
  611. (is_stored_as_bytes, user_id) = payload[0]
  612. if is_stored_as_bytes:
  613. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  614. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  615. system = payload[2]
  616. expires_at_str = cls._convert_float_to_time_string(payload[3])
  617. audit_ids = list(map(cls.base64_encode, payload[4]))
  618. project_id = None
  619. domain_id = None
  620. trust_id = None
  621. federated_group_ids = None
  622. identity_provider_id = None
  623. protocol_id = None
  624. access_token_id = None
  625. app_cred_id = None
  626. return (user_id, methods, system, project_id, domain_id,
  627. expires_at_str, audit_ids, trust_id, federated_group_ids,
  628. identity_provider_id, protocol_id, access_token_id,
  629. app_cred_id)
  630. class ApplicationCredentialScopedPayload(BasePayload):
  631. version = 9
  632. @classmethod
  633. def assemble(cls, user_id, methods, system, project_id, domain_id,
  634. expires_at, audit_ids, trust_id, federated_group_ids,
  635. identity_provider_id, protocol_id, access_token_id,
  636. app_cred_id):
  637. b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
  638. methods = auth_plugins.convert_method_list_to_integer(methods)
  639. b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
  640. expires_at_int = cls._convert_time_string_to_float(expires_at)
  641. b_audit_ids = list(map(cls.random_urlsafe_str_to_bytes,
  642. audit_ids))
  643. b_app_cred_id = cls.attempt_convert_uuid_hex_to_bytes(app_cred_id)
  644. return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids,
  645. b_app_cred_id)
  646. @classmethod
  647. def disassemble(cls, payload):
  648. (is_stored_as_bytes, user_id) = payload[0]
  649. if is_stored_as_bytes:
  650. user_id = cls.convert_uuid_bytes_to_hex(user_id)
  651. methods = auth_plugins.convert_integer_to_method_list(payload[1])
  652. (is_stored_as_bytes, project_id) = payload[2]
  653. if is_stored_as_bytes:
  654. project_id = cls.convert_uuid_bytes_to_hex(project_id)
  655. expires_at_str = cls._convert_float_to_time_string(payload[3])
  656. audit_ids = list(map(cls.base64_encode, payload[4]))
  657. system = None
  658. domain_id = None
  659. trust_id = None
  660. federated_group_ids = None
  661. identity_provider_id = None
  662. protocol_id = None
  663. access_token_id = None
  664. (is_stored_as_bytes, app_cred_id) = payload[5]
  665. if is_stored_as_bytes:
  666. app_cred_id = cls.convert_uuid_bytes_to_hex(app_cred_id)
  667. return (user_id, methods, system, project_id, domain_id,
  668. expires_at_str, audit_ids, trust_id, federated_group_ids,
  669. identity_provider_id, protocol_id, access_token_id,
  670. app_cred_id)
  671. _PAYLOAD_CLASSES = [
  672. UnscopedPayload,
  673. DomainScopedPayload,
  674. ProjectScopedPayload,
  675. TrustScopedPayload,
  676. FederatedUnscopedPayload,
  677. FederatedProjectScopedPayload,
  678. FederatedDomainScopedPayload,
  679. OauthScopedPayload,
  680. SystemScopedPayload,
  681. ApplicationCredentialScopedPayload,
  682. ]