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.

650 lines
18 KiB

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