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.

408 lines
12 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. # The MIT License (MIT)
  4. #
  5. # Copyright (C) 2017 Marcos Pereira <marcospereira.mpj@gmail.com>
  6. #
  7. # Permission is hereby granted, free of charge, to any person obtaining a copy of
  8. # this software and associated documentation files (the "Software"), to deal in
  9. # the Software without restriction, including without limitation the rights to
  10. # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  11. # the Software, and to permit persons to whom the Software is furnished to do so,
  12. # subject to the following conditions:
  13. #
  14. # The above copyright notice and this permission notice shall be included in all
  15. # copies or substantial portions of the Software.
  16. #
  17. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  19. # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  20. # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  21. # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  22. # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  23. """Keycloak OpenID Connection Manager module.
  24. The module contains mainly the implementation of KeycloakOpenIDConnection class.
  25. This is an extension of the ConnectionManager class, and handles the automatic refresh
  26. of openid tokens when required.
  27. """
  28. from datetime import datetime, timedelta
  29. from .connection import ConnectionManager
  30. from .exceptions import KeycloakPostError
  31. from .keycloak_openid import KeycloakOpenID
  32. class KeycloakOpenIDConnection(ConnectionManager):
  33. """A class to help with OpenID connections which can auto refresh tokens.
  34. :param object: _description_
  35. :type object: _type_
  36. """
  37. _server_url = None
  38. _username = None
  39. _password = None
  40. _totp = None
  41. _realm_name = None
  42. _client_id = None
  43. _verify = None
  44. _client_secret_key = None
  45. _connection = None
  46. _custom_headers = None
  47. _user_realm_name = None
  48. _expires_at = None
  49. def __init__(
  50. self,
  51. server_url,
  52. username=None,
  53. password=None,
  54. token=None,
  55. totp=None,
  56. realm_name="master",
  57. client_id="admin-cli",
  58. verify=True,
  59. client_secret_key=None,
  60. custom_headers=None,
  61. user_realm_name=None,
  62. timeout=60,
  63. ):
  64. """Init method.
  65. :param server_url: Keycloak server url
  66. :type server_url: str
  67. :param username: admin username
  68. :type username: str
  69. :param password: admin password
  70. :type password: str
  71. :param token: access and refresh tokens
  72. :type token: dict
  73. :param totp: Time based OTP
  74. :type totp: str
  75. :param realm_name: realm name
  76. :type realm_name: str
  77. :param client_id: client id
  78. :type client_id: str
  79. :param verify: True if want check connection SSL
  80. :type verify: bool
  81. :param client_secret_key: client secret key
  82. (optional, required only for access type confidential)
  83. :type client_secret_key: str
  84. :param custom_headers: dict of custom header to pass to each HTML request
  85. :type custom_headers: dict
  86. :param user_realm_name: The realm name of the user, if different from realm_name
  87. :type user_realm_name: str
  88. :param timeout: connection timeout in seconds
  89. :type timeout: int
  90. """
  91. # token is renewed when it hits 90% of its lifetime. This is to account for any possible
  92. # clock skew.
  93. self.token_lifetime_fraction = 0.9
  94. self.server_url = server_url
  95. self.username = username
  96. self.password = password
  97. self.token = token
  98. self.totp = totp
  99. self.realm_name = realm_name
  100. self.client_id = client_id
  101. self.verify = verify
  102. self.client_secret_key = client_secret_key
  103. self.user_realm_name = user_realm_name
  104. self.timeout = timeout
  105. if self.token is None:
  106. self.get_token()
  107. self.headers = (
  108. {
  109. "Authorization": "Bearer " + self.token.get("access_token"),
  110. "Content-Type": "application/json",
  111. }
  112. if self.token is not None
  113. else {}
  114. )
  115. self.custom_headers = custom_headers
  116. super().__init__(
  117. base_url=self.server_url, headers=self.headers, timeout=60, verify=self.verify
  118. )
  119. @property
  120. def server_url(self):
  121. """Get server url.
  122. :returns: Keycloak server url
  123. :rtype: str
  124. """
  125. return self.base_url
  126. @server_url.setter
  127. def server_url(self, value):
  128. self.base_url = value
  129. @property
  130. def realm_name(self):
  131. """Get realm name.
  132. :returns: Realm name
  133. :rtype: str
  134. """
  135. return self._realm_name
  136. @realm_name.setter
  137. def realm_name(self, value):
  138. self._realm_name = value
  139. @property
  140. def client_id(self):
  141. """Get client id.
  142. :returns: Client id
  143. :rtype: str
  144. """
  145. return self._client_id
  146. @client_id.setter
  147. def client_id(self, value):
  148. self._client_id = value
  149. @property
  150. def client_secret_key(self):
  151. """Get client secret key.
  152. :returns: Client secret key
  153. :rtype: str
  154. """
  155. return self._client_secret_key
  156. @client_secret_key.setter
  157. def client_secret_key(self, value):
  158. self._client_secret_key = value
  159. @property
  160. def username(self):
  161. """Get username.
  162. :returns: Admin username
  163. :rtype: str
  164. """
  165. return self._username
  166. @username.setter
  167. def username(self, value):
  168. self._username = value
  169. @property
  170. def password(self):
  171. """Get password.
  172. :returns: Admin password
  173. :rtype: str
  174. """
  175. return self._password
  176. @password.setter
  177. def password(self, value):
  178. self._password = value
  179. @property
  180. def totp(self):
  181. """Get totp.
  182. :returns: TOTP
  183. :rtype: str
  184. """
  185. return self._totp
  186. @totp.setter
  187. def totp(self, value):
  188. self._totp = value
  189. @property
  190. def token(self):
  191. """Get token.
  192. :returns: Access and refresh token
  193. :rtype: dict
  194. """
  195. return self._token
  196. @token.setter
  197. def token(self, value):
  198. self._token = value
  199. self._expires_at = datetime.now() + timedelta(
  200. seconds=int(self.token_lifetime_fraction * self.token["expires_in"] if value else 0)
  201. )
  202. @property
  203. def expires_at(self):
  204. """Get token expiry time.
  205. :returns: Datetime at which the current token will expire
  206. :rtype: datetime
  207. """
  208. return self._expires_at
  209. @property
  210. def user_realm_name(self):
  211. """Get user realm name.
  212. :returns: User realm name
  213. :rtype: str
  214. """
  215. return self._user_realm_name
  216. @user_realm_name.setter
  217. def user_realm_name(self, value):
  218. self._user_realm_name = value
  219. @property
  220. def custom_headers(self):
  221. """Get custom headers.
  222. :returns: Custom headers
  223. :rtype: dict
  224. """
  225. return self._custom_headers
  226. @custom_headers.setter
  227. def custom_headers(self, value):
  228. self._custom_headers = value
  229. if self.custom_headers is not None:
  230. # merge custom headers to main headers
  231. self.headers.update(self.custom_headers)
  232. def get_token(self):
  233. """Get admin token.
  234. The admin token is then set in the `token` attribute.
  235. """
  236. if self.user_realm_name:
  237. token_realm_name = self.user_realm_name
  238. elif self.realm_name:
  239. token_realm_name = self.realm_name
  240. else:
  241. token_realm_name = "master"
  242. self.keycloak_openid = KeycloakOpenID(
  243. server_url=self.server_url,
  244. client_id=self.client_id,
  245. realm_name=token_realm_name,
  246. verify=self.verify,
  247. client_secret_key=self.client_secret_key,
  248. timeout=self.timeout,
  249. )
  250. grant_type = []
  251. if self.client_secret_key:
  252. if self.user_realm_name:
  253. self.realm_name = self.user_realm_name
  254. grant_type.append("client_credentials")
  255. elif self.username and self.password:
  256. grant_type.append("password")
  257. if grant_type:
  258. self.token = self.keycloak_openid.token(
  259. self.username, self.password, grant_type=grant_type, totp=self.totp
  260. )
  261. else:
  262. self.token = None
  263. def refresh_token(self):
  264. """Refresh the token.
  265. :raises KeycloakPostError: In case the refresh token request failed.
  266. """
  267. refresh_token = self.token.get("refresh_token", None) if self.token else None
  268. if refresh_token is None:
  269. self.get_token()
  270. else:
  271. try:
  272. self.token = self.keycloak_openid.refresh_token(refresh_token)
  273. except KeycloakPostError as e:
  274. list_errors = [
  275. b"Refresh token expired",
  276. b"Token is not active",
  277. b"Session not active",
  278. ]
  279. if e.response_code == 400 and any(err in e.response_body for err in list_errors):
  280. self.get_token()
  281. else:
  282. raise
  283. self.add_param_headers("Authorization", "Bearer " + self.token.get("access_token"))
  284. def _refresh_if_required(self):
  285. if datetime.now() >= self.expires_at:
  286. self.refresh_token()
  287. def raw_get(self, *args, **kwargs):
  288. """Call connection.raw_get.
  289. If auto_refresh is set for *get* and *access_token* is expired, it will refresh the token
  290. and try *get* once more.
  291. :param args: Additional arguments
  292. :type args: tuple
  293. :param kwargs: Additional keyword arguments
  294. :type kwargs: dict
  295. :returns: Response
  296. :rtype: Response
  297. """
  298. self._refresh_if_required()
  299. r = super().raw_get(*args, **kwargs)
  300. return r
  301. def raw_post(self, *args, **kwargs):
  302. """Call connection.raw_post.
  303. If auto_refresh is set for *post* and *access_token* is expired, it will refresh the token
  304. and try *post* once more.
  305. :param args: Additional arguments
  306. :type args: tuple
  307. :param kwargs: Additional keyword arguments
  308. :type kwargs: dict
  309. :returns: Response
  310. :rtype: Response
  311. """
  312. self._refresh_if_required()
  313. r = super().raw_post(*args, **kwargs)
  314. return r
  315. def raw_put(self, *args, **kwargs):
  316. """Call connection.raw_put.
  317. If auto_refresh is set for *put* and *access_token* is expired, it will refresh the token
  318. and try *put* once more.
  319. :param args: Additional arguments
  320. :type args: tuple
  321. :param kwargs: Additional keyword arguments
  322. :type kwargs: dict
  323. :returns: Response
  324. :rtype: Response
  325. """
  326. self._refresh_if_required()
  327. r = super().raw_put(*args, **kwargs)
  328. return r
  329. def raw_delete(self, *args, **kwargs):
  330. """Call connection.raw_delete.
  331. If auto_refresh is set for *delete* and *access_token* is expired,
  332. it will refresh the token and try *delete* once more.
  333. :param args: Additional arguments
  334. :type args: tuple
  335. :param kwargs: Additional keyword arguments
  336. :type kwargs: dict
  337. :returns: Response
  338. :rtype: Response
  339. """
  340. self._refresh_if_required()
  341. r = super().raw_delete(*args, **kwargs)
  342. return r