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.

608 lines
17 KiB

3 years ago
1 month ago
1 month ago
3 years ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
  1. """Fixtures for tests."""
  2. import ipaddress
  3. import os
  4. import uuid
  5. from collections.abc import Generator
  6. from datetime import datetime, timedelta
  7. from typing import Tuple
  8. import freezegun
  9. import pytest
  10. from cryptography import x509
  11. from cryptography.hazmat.backends import default_backend
  12. from cryptography.hazmat.primitives import hashes, serialization
  13. from cryptography.hazmat.primitives.asymmetric import rsa
  14. from cryptography.x509.oid import NameOID
  15. from keycloak import KeycloakAdmin, KeycloakOpenID, KeycloakOpenIDConnection, KeycloakUMA
  16. class KeycloakTestEnv:
  17. """Wrapper for test Keycloak connection configuration.
  18. :param host: Hostname
  19. :type host: str
  20. :param port: Port
  21. :type port: str
  22. :param username: Admin username
  23. :type username: str
  24. :param password: Admin password
  25. :type password: str
  26. """
  27. def __init__(
  28. self,
  29. host: str = os.environ["KEYCLOAK_HOST"],
  30. port: str = os.environ["KEYCLOAK_PORT"],
  31. username: str = os.environ["KEYCLOAK_ADMIN"],
  32. password: str = os.environ["KEYCLOAK_ADMIN_PASSWORD"],
  33. ):
  34. """Init method.
  35. :param host: Hostname
  36. :type host: str
  37. :param port: Port
  38. :type port: str
  39. :param username: Admin username
  40. :type username: str
  41. :param password: Admin password
  42. :type password: str
  43. """
  44. self.KEYCLOAK_HOST = host
  45. self.KEYCLOAK_PORT = port
  46. self.KEYCLOAK_ADMIN = username
  47. self.KEYCLOAK_ADMIN_PASSWORD = password
  48. @property
  49. def KEYCLOAK_HOST(self):
  50. """Hostname getter.
  51. :returns: Keycloak host
  52. :rtype: str
  53. """
  54. return self._KEYCLOAK_HOST
  55. @KEYCLOAK_HOST.setter
  56. def KEYCLOAK_HOST(self, value: str):
  57. """Hostname setter.
  58. :param value: Keycloak host
  59. :type value: str
  60. """
  61. self._KEYCLOAK_HOST = value
  62. @property
  63. def KEYCLOAK_PORT(self):
  64. """Port getter.
  65. :returns: Keycloak port
  66. :rtype: str
  67. """
  68. return self._KEYCLOAK_PORT
  69. @KEYCLOAK_PORT.setter
  70. def KEYCLOAK_PORT(self, value: str):
  71. """Port setter.
  72. :param value: Keycloak port
  73. :type value: str
  74. """
  75. self._KEYCLOAK_PORT = value
  76. @property
  77. def KEYCLOAK_ADMIN(self):
  78. """Admin username getter.
  79. :returns: Admin username
  80. :rtype: str
  81. """
  82. return self._KEYCLOAK_ADMIN
  83. @KEYCLOAK_ADMIN.setter
  84. def KEYCLOAK_ADMIN(self, value: str):
  85. """Admin username setter.
  86. :param value: Admin username
  87. :type value: str
  88. """
  89. self._KEYCLOAK_ADMIN = value
  90. @property
  91. def KEYCLOAK_ADMIN_PASSWORD(self):
  92. """Admin password getter.
  93. :returns: Admin password
  94. :rtype: str
  95. """
  96. return self._KEYCLOAK_ADMIN_PASSWORD
  97. @KEYCLOAK_ADMIN_PASSWORD.setter
  98. def KEYCLOAK_ADMIN_PASSWORD(self, value: str):
  99. """Admin password setter.
  100. :param value: Admin password
  101. :type value: str
  102. """
  103. self._KEYCLOAK_ADMIN_PASSWORD = value
  104. @pytest.fixture
  105. def env():
  106. """Fixture for getting the test environment configuration object.
  107. :returns: Keycloak test environment object
  108. :rtype: KeycloakTestEnv
  109. """
  110. return KeycloakTestEnv()
  111. @pytest.fixture
  112. def admin(env: KeycloakTestEnv):
  113. """Fixture for initialized KeycloakAdmin class.
  114. :param env: Keycloak test environment
  115. :type env: KeycloakTestEnv
  116. :returns: Keycloak admin
  117. :rtype: KeycloakAdmin
  118. """
  119. return KeycloakAdmin(
  120. server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
  121. username=env.KEYCLOAK_ADMIN,
  122. password=env.KEYCLOAK_ADMIN_PASSWORD,
  123. )
  124. @pytest.fixture
  125. @freezegun.freeze_time("2023-02-25 10:00:00")
  126. def admin_frozen(env: KeycloakTestEnv):
  127. """Fixture for initialized KeycloakAdmin class, with time frozen.
  128. :param env: Keycloak test environment
  129. :type env: KeycloakTestEnv
  130. :returns: Keycloak admin
  131. :rtype: KeycloakAdmin
  132. """
  133. return KeycloakAdmin(
  134. server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
  135. username=env.KEYCLOAK_ADMIN,
  136. password=env.KEYCLOAK_ADMIN_PASSWORD,
  137. )
  138. @pytest.fixture
  139. def oid(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin):
  140. """Fixture for initialized KeycloakOpenID class.
  141. :param env: Keycloak test environment
  142. :type env: KeycloakTestEnv
  143. :param realm: Keycloak realm
  144. :type realm: str
  145. :param admin: Keycloak admin
  146. :type admin: KeycloakAdmin
  147. :yields: Keycloak OpenID client
  148. :rtype: KeycloakOpenID
  149. """
  150. # Set the realm
  151. admin.change_current_realm(realm)
  152. # Create client
  153. client = str(uuid.uuid4())
  154. client_id = admin.create_client(
  155. payload={
  156. "name": client,
  157. "clientId": client,
  158. "enabled": True,
  159. "publicClient": True,
  160. "protocol": "openid-connect",
  161. },
  162. )
  163. # Return OID
  164. yield KeycloakOpenID(
  165. server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
  166. realm_name=realm,
  167. client_id=client,
  168. )
  169. # Cleanup
  170. admin.delete_client(client_id=client_id)
  171. @pytest.fixture
  172. def oid_with_credentials(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin):
  173. """Fixture for an initialized KeycloakOpenID class and a random user credentials.
  174. :param env: Keycloak test environment
  175. :type env: KeycloakTestEnv
  176. :param realm: Keycloak realm
  177. :type realm: str
  178. :param admin: Keycloak admin
  179. :type admin: KeycloakAdmin
  180. :yields: Keycloak OpenID client with user credentials
  181. :rtype: Tuple[KeycloakOpenID, str, str]
  182. """
  183. # Set the realm
  184. admin.change_current_realm(realm)
  185. # Create client
  186. client = str(uuid.uuid4())
  187. secret = str(uuid.uuid4())
  188. client_id = admin.create_client(
  189. payload={
  190. "name": client,
  191. "clientId": client,
  192. "enabled": True,
  193. "publicClient": False,
  194. "protocol": "openid-connect",
  195. "secret": secret,
  196. "clientAuthenticatorType": "client-secret",
  197. },
  198. )
  199. # Create user
  200. username = str(uuid.uuid4())
  201. password = str(uuid.uuid4())
  202. user_id = admin.create_user(
  203. payload={
  204. "username": username,
  205. "email": f"{username}@test.test",
  206. "enabled": True,
  207. "firstName": "first",
  208. "lastName": "last",
  209. "emailVerified": True,
  210. "requiredActions": [],
  211. "credentials": [{"type": "password", "value": password, "temporary": False}],
  212. },
  213. )
  214. yield (
  215. KeycloakOpenID(
  216. server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
  217. realm_name=realm,
  218. client_id=client,
  219. client_secret_key=secret,
  220. ),
  221. username,
  222. password,
  223. )
  224. # Cleanup
  225. admin.delete_client(client_id=client_id)
  226. admin.delete_user(user_id=user_id)
  227. @pytest.fixture
  228. def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin):
  229. """Fixture for an initialized KeycloakOpenID class and a random user credentials.
  230. :param env: Keycloak test environment
  231. :type env: KeycloakTestEnv
  232. :param realm: Keycloak realm
  233. :type realm: str
  234. :param admin: Keycloak admin
  235. :type admin: KeycloakAdmin
  236. :yields: Keycloak OpenID client configured as an authorization server with client credentials
  237. :rtype: Tuple[KeycloakOpenID, str, str]
  238. """
  239. # Set the realm
  240. admin.change_current_realm(realm)
  241. # Create client
  242. client = str(uuid.uuid4())
  243. secret = str(uuid.uuid4())
  244. client_id = admin.create_client(
  245. payload={
  246. "name": client,
  247. "clientId": client,
  248. "enabled": True,
  249. "publicClient": False,
  250. "protocol": "openid-connect",
  251. "secret": secret,
  252. "clientAuthenticatorType": "client-secret",
  253. "authorizationServicesEnabled": True,
  254. "serviceAccountsEnabled": True,
  255. },
  256. )
  257. admin.create_client_authz_role_based_policy(
  258. client_id=client_id,
  259. payload={
  260. "name": "test-authz-rb-policy",
  261. "roles": [{"id": admin.get_realm_role(role_name="offline_access")["id"]}],
  262. },
  263. )
  264. # Create user
  265. username = str(uuid.uuid4())
  266. password = str(uuid.uuid4())
  267. user_id = admin.create_user(
  268. payload={
  269. "username": username,
  270. "email": f"{username}@test.test",
  271. "enabled": True,
  272. "emailVerified": True,
  273. "firstName": "first",
  274. "lastName": "last",
  275. "requiredActions": [],
  276. "credentials": [{"type": "password", "value": password, "temporary": False}],
  277. },
  278. )
  279. yield (
  280. KeycloakOpenID(
  281. server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
  282. realm_name=realm,
  283. client_id=client,
  284. client_secret_key=secret,
  285. ),
  286. username,
  287. password,
  288. )
  289. # Cleanup
  290. admin.delete_client(client_id=client_id)
  291. admin.delete_user(user_id=user_id)
  292. @pytest.fixture
  293. def oid_with_credentials_device(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin):
  294. """Fixture for an initialized KeycloakOpenID class and a random user credentials.
  295. :param env: Keycloak test environment
  296. :type env: KeycloakTestEnv
  297. :param realm: Keycloak realm
  298. :type realm: str
  299. :param admin: Keycloak admin
  300. :type admin: KeycloakAdmin
  301. :yields: Keycloak OpenID client with user credentials
  302. :rtype: Tuple[KeycloakOpenID, str, str]
  303. """
  304. # Set the realm
  305. admin.change_current_realm(realm)
  306. # Create client
  307. client = str(uuid.uuid4())
  308. secret = str(uuid.uuid4())
  309. client_id = admin.create_client(
  310. payload={
  311. "name": client,
  312. "clientId": client,
  313. "enabled": True,
  314. "publicClient": False,
  315. "protocol": "openid-connect",
  316. "secret": secret,
  317. "clientAuthenticatorType": "client-secret",
  318. "attributes": {"oauth2.device.authorization.grant.enabled": True},
  319. },
  320. )
  321. # Create user
  322. username = str(uuid.uuid4())
  323. password = str(uuid.uuid4())
  324. user_id = admin.create_user(
  325. payload={
  326. "username": username,
  327. "email": f"{username}@test.test",
  328. "enabled": True,
  329. "firstName": "first",
  330. "lastName": "last",
  331. "emailVerified": True,
  332. "requiredActions": [],
  333. "credentials": [{"type": "password", "value": password, "temporary": False}],
  334. },
  335. )
  336. yield (
  337. KeycloakOpenID(
  338. server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
  339. realm_name=realm,
  340. client_id=client,
  341. client_secret_key=secret,
  342. ),
  343. username,
  344. password,
  345. )
  346. # Cleanup
  347. admin.delete_client(client_id=client_id)
  348. admin.delete_user(user_id=user_id)
  349. @pytest.fixture
  350. def realm(admin: KeycloakAdmin) -> Generator[str, None, None]:
  351. """Fixture for a new random realm.
  352. :param admin: Keycloak admin
  353. :type admin: KeycloakAdmin
  354. :yields: Keycloak realm
  355. :rtype: str
  356. """
  357. realm_name = str(uuid.uuid4())
  358. admin.create_realm(payload={"realm": realm_name, "enabled": True})
  359. yield realm_name
  360. admin.delete_realm(realm_name=realm_name)
  361. @pytest.fixture
  362. def user(admin: KeycloakAdmin, realm: str) -> Generator[str, None, None]:
  363. """Fixture for a new random user.
  364. :param admin: Keycloak admin
  365. :type admin: KeycloakAdmin
  366. :param realm: Keycloak realm
  367. :type realm: str
  368. :yields: Keycloak user
  369. :rtype: str
  370. """
  371. admin.change_current_realm(realm)
  372. username = str(uuid.uuid4())
  373. user_id = admin.create_user(payload={"username": username, "email": f"{username}@test.test"})
  374. yield user_id
  375. admin.delete_user(user_id=user_id)
  376. @pytest.fixture
  377. def group(admin: KeycloakAdmin, realm: str) -> Generator[str, None, None]:
  378. """Fixture for a new random group.
  379. :param admin: Keycloak admin
  380. :type admin: KeycloakAdmin
  381. :param realm: Keycloak realm
  382. :type realm: str
  383. :yields: Keycloak group
  384. :rtype: str
  385. """
  386. admin.change_current_realm(realm)
  387. group_name = str(uuid.uuid4())
  388. group_id = admin.create_group(payload={"name": group_name})
  389. yield group_id
  390. admin.delete_group(group_id=group_id)
  391. @pytest.fixture
  392. def client(admin: KeycloakAdmin, realm: str) -> Generator[str, None, None]:
  393. """Fixture for a new random client.
  394. :param admin: Keycloak admin
  395. :type admin: KeycloakAdmin
  396. :param realm: Keycloak realm
  397. :type realm: str
  398. :yields: Keycloak client id
  399. :rtype: str
  400. """
  401. admin.change_current_realm(realm)
  402. client = str(uuid.uuid4())
  403. client_id = admin.create_client(payload={"name": client, "clientId": client})
  404. yield client_id
  405. admin.delete_client(client_id=client_id)
  406. @pytest.fixture
  407. def client_role(admin: KeycloakAdmin, realm: str, client: str) -> Generator[str, None, None]:
  408. """Fixture for a new random client role.
  409. :param admin: Keycloak admin
  410. :type admin: KeycloakAdmin
  411. :param realm: Keycloak realm
  412. :type realm: str
  413. :param client: Keycloak client
  414. :type client: str
  415. :yields: Keycloak client role
  416. :rtype: str
  417. """
  418. admin.change_current_realm(realm)
  419. role = str(uuid.uuid4())
  420. admin.create_client_role(client, {"name": role, "composite": False})
  421. yield role
  422. admin.delete_client_role(client, role)
  423. @pytest.fixture
  424. def composite_client_role(
  425. admin: KeycloakAdmin,
  426. realm: str,
  427. client: str,
  428. client_role: str,
  429. ) -> Generator[str, None, None]:
  430. """Fixture for a new random composite client role.
  431. :param admin: Keycloak admin
  432. :type admin: KeycloakAdmin
  433. :param realm: Keycloak realm
  434. :type realm: str
  435. :param client: Keycloak client
  436. :type client: str
  437. :param client_role: Keycloak client role
  438. :type client_role: str
  439. :yields: Composite client role
  440. :rtype: str
  441. """
  442. admin.change_current_realm(realm)
  443. role = str(uuid.uuid4())
  444. admin.create_client_role(client, {"name": role, "composite": True})
  445. role_repr = admin.get_client_role(client, client_role)
  446. admin.add_composite_client_roles_to_role(client, role, roles=[role_repr])
  447. yield role
  448. admin.delete_client_role(client, role)
  449. @pytest.fixture
  450. def selfsigned_cert():
  451. """Generate self signed certificate for a hostname, and optional IP addresses.
  452. :returns: Selfsigned certificate
  453. :rtype: Tuple[str, str]
  454. """
  455. hostname = "testcert"
  456. ip_addresses = None
  457. key = None
  458. # Generate our key
  459. if key is None:
  460. key = rsa.generate_private_key(
  461. public_exponent=65537,
  462. key_size=2048,
  463. backend=default_backend(),
  464. )
  465. name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, hostname)])
  466. alt_names = [x509.DNSName(hostname)]
  467. # allow addressing by IP, for when you don't have real DNS (common in most testing scenarios
  468. if ip_addresses:
  469. for addr in ip_addresses:
  470. # openssl wants DNSnames for ips...
  471. alt_names.append(x509.DNSName(addr))
  472. # ... whereas golang's crypto/tls is stricter, and needs IPAddresses
  473. # note: older versions of cryptography do not understand ip_address objects
  474. alt_names.append(x509.IPAddress(ipaddress.ip_address(addr)))
  475. san = x509.SubjectAlternativeName(alt_names)
  476. # path_len=0 means this cert can only sign itself, not other certs.
  477. basic_contraints = x509.BasicConstraints(ca=True, path_length=0)
  478. now = datetime.utcnow()
  479. cert = (
  480. x509.CertificateBuilder()
  481. .subject_name(name)
  482. .issuer_name(name)
  483. .public_key(key.public_key())
  484. .serial_number(1000)
  485. .not_valid_before(now)
  486. .not_valid_after(now + timedelta(days=10 * 365))
  487. .add_extension(basic_contraints, False)
  488. .add_extension(san, False)
  489. .sign(key, hashes.SHA256(), default_backend())
  490. )
  491. cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
  492. key_pem = key.private_bytes(
  493. encoding=serialization.Encoding.PEM,
  494. format=serialization.PrivateFormat.TraditionalOpenSSL,
  495. encryption_algorithm=serialization.NoEncryption(),
  496. )
  497. return cert_pem, key_pem
  498. @pytest.fixture
  499. def oid_connection_with_authz(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
  500. """Fixture for initialized KeycloakUMA class.
  501. :param oid_with_credentials_authz: Keycloak OpenID client with pre-configured user credentials
  502. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  503. :yields: Keycloak OpenID connection manager
  504. :rtype: KeycloakOpenIDConnection
  505. """
  506. oid, _, _ = oid_with_credentials_authz
  507. connection = KeycloakOpenIDConnection(
  508. server_url=oid.connection.base_url,
  509. realm_name=oid.realm_name,
  510. client_id=oid.client_id,
  511. client_secret_key=oid.client_secret_key,
  512. timeout=60,
  513. )
  514. return connection
  515. @pytest.fixture
  516. def uma(oid_connection_with_authz: KeycloakOpenIDConnection):
  517. """Fixture for initialized KeycloakUMA class.
  518. :param oid_connection_with_authz: Keycloak open id connection with pre-configured authz client
  519. :type oid_connection_with_authz: KeycloakOpenIDConnection
  520. :yields: Keycloak OpenID client
  521. :rtype: KeycloakOpenID
  522. """
  523. connection = oid_connection_with_authz
  524. # Return UMA
  525. return KeycloakUMA(connection=connection)