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.

530 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.realm_name = 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.realm_name = 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. "credentials": [{"type": "password", "value": password}],
  207. }
  208. )
  209. yield (
  210. KeycloakOpenID(
  211. server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
  212. realm_name=realm,
  213. client_id=client,
  214. client_secret_key=secret,
  215. ),
  216. username,
  217. password,
  218. )
  219. # Cleanup
  220. admin.delete_client(client_id=client_id)
  221. admin.delete_user(user_id=user_id)
  222. @pytest.fixture
  223. def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin):
  224. """Fixture for an initialized KeycloakOpenID class and a random user credentials.
  225. :param env: Keycloak test environment
  226. :type env: KeycloakTestEnv
  227. :param realm: Keycloak realm
  228. :type realm: str
  229. :param admin: Keycloak admin
  230. :type admin: KeycloakAdmin
  231. :yields: Keycloak OpenID client configured as an authorization server with client credentials
  232. :rtype: Tuple[KeycloakOpenID, str, str]
  233. """
  234. # Set the realm
  235. admin.realm_name = realm
  236. # Create client
  237. client = str(uuid.uuid4())
  238. secret = str(uuid.uuid4())
  239. client_id = admin.create_client(
  240. payload={
  241. "name": client,
  242. "clientId": client,
  243. "enabled": True,
  244. "publicClient": False,
  245. "protocol": "openid-connect",
  246. "secret": secret,
  247. "clientAuthenticatorType": "client-secret",
  248. "authorizationServicesEnabled": True,
  249. "serviceAccountsEnabled": True,
  250. }
  251. )
  252. admin.create_client_authz_role_based_policy(
  253. client_id=client_id,
  254. payload={
  255. "name": "test-authz-rb-policy",
  256. "roles": [{"id": admin.get_realm_role(role_name="offline_access")["id"]}],
  257. },
  258. )
  259. # Create user
  260. username = str(uuid.uuid4())
  261. password = str(uuid.uuid4())
  262. user_id = admin.create_user(
  263. payload={
  264. "username": username,
  265. "email": f"{username}@test.test",
  266. "enabled": True,
  267. "credentials": [{"type": "password", "value": password}],
  268. }
  269. )
  270. yield (
  271. KeycloakOpenID(
  272. server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
  273. realm_name=realm,
  274. client_id=client,
  275. client_secret_key=secret,
  276. ),
  277. username,
  278. password,
  279. )
  280. # Cleanup
  281. admin.delete_client(client_id=client_id)
  282. admin.delete_user(user_id=user_id)
  283. @pytest.fixture
  284. def realm(admin: KeycloakAdmin) -> str:
  285. """Fixture for a new random realm.
  286. :param admin: Keycloak admin
  287. :type admin: KeycloakAdmin
  288. :yields: Keycloak realm
  289. :rtype: str
  290. """
  291. realm_name = str(uuid.uuid4())
  292. admin.create_realm(payload={"realm": realm_name, "enabled": True})
  293. yield realm_name
  294. admin.delete_realm(realm_name=realm_name)
  295. @pytest.fixture
  296. def user(admin: KeycloakAdmin, realm: str) -> str:
  297. """Fixture for a new random user.
  298. :param admin: Keycloak admin
  299. :type admin: KeycloakAdmin
  300. :param realm: Keycloak realm
  301. :type realm: str
  302. :yields: Keycloak user
  303. :rtype: str
  304. """
  305. admin.realm_name = realm
  306. username = str(uuid.uuid4())
  307. user_id = admin.create_user(payload={"username": username, "email": f"{username}@test.test"})
  308. yield user_id
  309. admin.delete_user(user_id=user_id)
  310. @pytest.fixture
  311. def group(admin: KeycloakAdmin, realm: str) -> str:
  312. """Fixture for a new random group.
  313. :param admin: Keycloak admin
  314. :type admin: KeycloakAdmin
  315. :param realm: Keycloak realm
  316. :type realm: str
  317. :yields: Keycloak group
  318. :rtype: str
  319. """
  320. admin.realm_name = realm
  321. group_name = str(uuid.uuid4())
  322. group_id = admin.create_group(payload={"name": group_name})
  323. yield group_id
  324. admin.delete_group(group_id=group_id)
  325. @pytest.fixture
  326. def client(admin: KeycloakAdmin, realm: str) -> str:
  327. """Fixture for a new random client.
  328. :param admin: Keycloak admin
  329. :type admin: KeycloakAdmin
  330. :param realm: Keycloak realm
  331. :type realm: str
  332. :yields: Keycloak client id
  333. :rtype: str
  334. """
  335. admin.realm_name = realm
  336. client = str(uuid.uuid4())
  337. client_id = admin.create_client(payload={"name": client, "clientId": client})
  338. yield client_id
  339. admin.delete_client(client_id=client_id)
  340. @pytest.fixture
  341. def client_role(admin: KeycloakAdmin, realm: str, client: str) -> str:
  342. """Fixture for a new random client role.
  343. :param admin: Keycloak admin
  344. :type admin: KeycloakAdmin
  345. :param realm: Keycloak realm
  346. :type realm: str
  347. :param client: Keycloak client
  348. :type client: str
  349. :yields: Keycloak client role
  350. :rtype: str
  351. """
  352. admin.realm_name = realm
  353. role = str(uuid.uuid4())
  354. admin.create_client_role(client, {"name": role, "composite": False})
  355. yield role
  356. admin.delete_client_role(client, role)
  357. @pytest.fixture
  358. def composite_client_role(admin: KeycloakAdmin, realm: str, client: str, client_role: str) -> str:
  359. """Fixture for a new random composite client role.
  360. :param admin: Keycloak admin
  361. :type admin: KeycloakAdmin
  362. :param realm: Keycloak realm
  363. :type realm: str
  364. :param client: Keycloak client
  365. :type client: str
  366. :param client_role: Keycloak client role
  367. :type client_role: str
  368. :yields: Composite client role
  369. :rtype: str
  370. """
  371. admin.realm_name = realm
  372. role = str(uuid.uuid4())
  373. admin.create_client_role(client, {"name": role, "composite": True})
  374. role_repr = admin.get_client_role(client, client_role)
  375. admin.add_composite_client_roles_to_role(client, role, roles=[role_repr])
  376. yield role
  377. admin.delete_client_role(client, role)
  378. @pytest.fixture
  379. def selfsigned_cert():
  380. """Generate self signed certificate for a hostname, and optional IP addresses.
  381. :returns: Selfsigned certificate
  382. :rtype: Tuple[str, str]
  383. """
  384. hostname = "testcert"
  385. ip_addresses = None
  386. key = None
  387. # Generate our key
  388. if key is None:
  389. key = rsa.generate_private_key(
  390. public_exponent=65537, key_size=2048, backend=default_backend()
  391. )
  392. name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, hostname)])
  393. alt_names = [x509.DNSName(hostname)]
  394. # allow addressing by IP, for when you don't have real DNS (common in most testing scenarios
  395. if ip_addresses:
  396. for addr in ip_addresses:
  397. # openssl wants DNSnames for ips...
  398. alt_names.append(x509.DNSName(addr))
  399. # ... whereas golang's crypto/tls is stricter, and needs IPAddresses
  400. # note: older versions of cryptography do not understand ip_address objects
  401. alt_names.append(x509.IPAddress(ipaddress.ip_address(addr)))
  402. san = x509.SubjectAlternativeName(alt_names)
  403. # path_len=0 means this cert can only sign itself, not other certs.
  404. basic_contraints = x509.BasicConstraints(ca=True, path_length=0)
  405. now = datetime.utcnow()
  406. cert = (
  407. x509.CertificateBuilder()
  408. .subject_name(name)
  409. .issuer_name(name)
  410. .public_key(key.public_key())
  411. .serial_number(1000)
  412. .not_valid_before(now)
  413. .not_valid_after(now + timedelta(days=10 * 365))
  414. .add_extension(basic_contraints, False)
  415. .add_extension(san, False)
  416. .sign(key, hashes.SHA256(), default_backend())
  417. )
  418. cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
  419. key_pem = key.private_bytes(
  420. encoding=serialization.Encoding.PEM,
  421. format=serialization.PrivateFormat.TraditionalOpenSSL,
  422. encryption_algorithm=serialization.NoEncryption(),
  423. )
  424. return cert_pem, key_pem
  425. @pytest.fixture
  426. def oid_connection_with_authz(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
  427. """Fixture for initialized KeycloakUMA class.
  428. :param oid_with_credentials_authz: Keycloak OpenID client with pre-configured user credentials
  429. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  430. :yields: Keycloak OpenID connection manager
  431. :rtype: KeycloakOpenIDConnection
  432. """
  433. oid, _, _ = oid_with_credentials_authz
  434. connection = KeycloakOpenIDConnection(
  435. server_url=oid.connection.base_url,
  436. realm_name=oid.realm_name,
  437. client_id=oid.client_id,
  438. client_secret_key=oid.client_secret_key,
  439. timeout=60,
  440. )
  441. yield connection
  442. @pytest.fixture
  443. def uma(oid_connection_with_authz: KeycloakOpenIDConnection):
  444. """Fixture for initialized KeycloakUMA class.
  445. :param oid_connection_with_authz: Keycloak open id connection with pre-configured authz client
  446. :type oid_connection_with_authz: KeycloakOpenIDConnection
  447. :yields: Keycloak OpenID client
  448. :rtype: KeycloakOpenID
  449. """
  450. connection = oid_connection_with_authz
  451. # Return UMA
  452. yield KeycloakUMA(connection=connection)