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.

602 lines
17 KiB

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