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.

538 lines
15 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 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 realm(admin: KeycloakAdmin) -> str:
  293. """Fixture for a new random realm.
  294. :param admin: Keycloak admin
  295. :type admin: KeycloakAdmin
  296. :yields: Keycloak realm
  297. :rtype: str
  298. """
  299. realm_name = str(uuid.uuid4())
  300. admin.create_realm(payload={"realm": realm_name, "enabled": True})
  301. yield realm_name
  302. admin.delete_realm(realm_name=realm_name)
  303. @pytest.fixture
  304. def user(admin: KeycloakAdmin, realm: str) -> str:
  305. """Fixture for a new random user.
  306. :param admin: Keycloak admin
  307. :type admin: KeycloakAdmin
  308. :param realm: Keycloak realm
  309. :type realm: str
  310. :yields: Keycloak user
  311. :rtype: str
  312. """
  313. admin.change_current_realm(realm)
  314. username = str(uuid.uuid4())
  315. user_id = admin.create_user(payload={"username": username, "email": f"{username}@test.test"})
  316. yield user_id
  317. admin.delete_user(user_id=user_id)
  318. @pytest.fixture
  319. def group(admin: KeycloakAdmin, realm: str) -> str:
  320. """Fixture for a new random group.
  321. :param admin: Keycloak admin
  322. :type admin: KeycloakAdmin
  323. :param realm: Keycloak realm
  324. :type realm: str
  325. :yields: Keycloak group
  326. :rtype: str
  327. """
  328. admin.change_current_realm(realm)
  329. group_name = str(uuid.uuid4())
  330. group_id = admin.create_group(payload={"name": group_name})
  331. yield group_id
  332. admin.delete_group(group_id=group_id)
  333. @pytest.fixture
  334. def client(admin: KeycloakAdmin, realm: str) -> str:
  335. """Fixture for a new random client.
  336. :param admin: Keycloak admin
  337. :type admin: KeycloakAdmin
  338. :param realm: Keycloak realm
  339. :type realm: str
  340. :yields: Keycloak client id
  341. :rtype: str
  342. """
  343. admin.change_current_realm(realm)
  344. client = str(uuid.uuid4())
  345. client_id = admin.create_client(payload={"name": client, "clientId": client})
  346. yield client_id
  347. admin.delete_client(client_id=client_id)
  348. @pytest.fixture
  349. def client_role(admin: KeycloakAdmin, realm: str, client: str) -> str:
  350. """Fixture for a new random client role.
  351. :param admin: Keycloak admin
  352. :type admin: KeycloakAdmin
  353. :param realm: Keycloak realm
  354. :type realm: str
  355. :param client: Keycloak client
  356. :type client: str
  357. :yields: Keycloak client role
  358. :rtype: str
  359. """
  360. admin.change_current_realm(realm)
  361. role = str(uuid.uuid4())
  362. admin.create_client_role(client, {"name": role, "composite": False})
  363. yield role
  364. admin.delete_client_role(client, role)
  365. @pytest.fixture
  366. def composite_client_role(admin: KeycloakAdmin, realm: str, client: str, client_role: str) -> str:
  367. """Fixture for a new random composite client role.
  368. :param admin: Keycloak admin
  369. :type admin: KeycloakAdmin
  370. :param realm: Keycloak realm
  371. :type realm: str
  372. :param client: Keycloak client
  373. :type client: str
  374. :param client_role: Keycloak client role
  375. :type client_role: str
  376. :yields: Composite client role
  377. :rtype: str
  378. """
  379. admin.change_current_realm(realm)
  380. role = str(uuid.uuid4())
  381. admin.create_client_role(client, {"name": role, "composite": True})
  382. role_repr = admin.get_client_role(client, client_role)
  383. admin.add_composite_client_roles_to_role(client, role, roles=[role_repr])
  384. yield role
  385. admin.delete_client_role(client, role)
  386. @pytest.fixture
  387. def selfsigned_cert():
  388. """Generate self signed certificate for a hostname, and optional IP addresses.
  389. :returns: Selfsigned certificate
  390. :rtype: Tuple[str, str]
  391. """
  392. hostname = "testcert"
  393. ip_addresses = None
  394. key = None
  395. # Generate our key
  396. if key is None:
  397. key = rsa.generate_private_key(
  398. public_exponent=65537, key_size=2048, backend=default_backend()
  399. )
  400. name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, hostname)])
  401. alt_names = [x509.DNSName(hostname)]
  402. # allow addressing by IP, for when you don't have real DNS (common in most testing scenarios
  403. if ip_addresses:
  404. for addr in ip_addresses:
  405. # openssl wants DNSnames for ips...
  406. alt_names.append(x509.DNSName(addr))
  407. # ... whereas golang's crypto/tls is stricter, and needs IPAddresses
  408. # note: older versions of cryptography do not understand ip_address objects
  409. alt_names.append(x509.IPAddress(ipaddress.ip_address(addr)))
  410. san = x509.SubjectAlternativeName(alt_names)
  411. # path_len=0 means this cert can only sign itself, not other certs.
  412. basic_contraints = x509.BasicConstraints(ca=True, path_length=0)
  413. now = datetime.utcnow()
  414. cert = (
  415. x509.CertificateBuilder()
  416. .subject_name(name)
  417. .issuer_name(name)
  418. .public_key(key.public_key())
  419. .serial_number(1000)
  420. .not_valid_before(now)
  421. .not_valid_after(now + timedelta(days=10 * 365))
  422. .add_extension(basic_contraints, False)
  423. .add_extension(san, False)
  424. .sign(key, hashes.SHA256(), default_backend())
  425. )
  426. cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
  427. key_pem = key.private_bytes(
  428. encoding=serialization.Encoding.PEM,
  429. format=serialization.PrivateFormat.TraditionalOpenSSL,
  430. encryption_algorithm=serialization.NoEncryption(),
  431. )
  432. return cert_pem, key_pem
  433. @pytest.fixture
  434. def oid_connection_with_authz(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
  435. """Fixture for initialized KeycloakUMA class.
  436. :param oid_with_credentials_authz: Keycloak OpenID client with pre-configured user credentials
  437. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  438. :yields: Keycloak OpenID connection manager
  439. :rtype: KeycloakOpenIDConnection
  440. """
  441. oid, _, _ = oid_with_credentials_authz
  442. connection = KeycloakOpenIDConnection(
  443. server_url=oid.connection.base_url,
  444. realm_name=oid.realm_name,
  445. client_id=oid.client_id,
  446. client_secret_key=oid.client_secret_key,
  447. timeout=60,
  448. )
  449. yield connection
  450. @pytest.fixture
  451. def uma(oid_connection_with_authz: KeycloakOpenIDConnection):
  452. """Fixture for initialized KeycloakUMA class.
  453. :param oid_connection_with_authz: Keycloak open id connection with pre-configured authz client
  454. :type oid_connection_with_authz: KeycloakOpenIDConnection
  455. :yields: Keycloak OpenID client
  456. :rtype: KeycloakOpenID
  457. """
  458. connection = oid_connection_with_authz
  459. # Return UMA
  460. yield KeycloakUMA(connection=connection)