Skip to content

Commit f7fcb98

Browse files
Merge pull request #106 from alip990/keycloak-organization
feat(keycloak): implement organization management methods and realm u…
2 parents 2bbaf04 + 35397d3 commit f7fcb98

5 files changed

Lines changed: 862 additions & 2 deletions

File tree

archipy/adapters/keycloak/adapters.py

Lines changed: 224 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import logging
33
import time
4-
from typing import Any, override
4+
from typing import Any, NoReturn, override
55

66
from async_lru import alru_cache
77
from keycloak import KeycloakAdmin, KeycloakOpenID
@@ -86,7 +86,7 @@ def _extract_error_message(cls, exception: KeycloakError) -> str:
8686
return error_message
8787

8888
@classmethod
89-
def _handle_keycloak_exception(cls, exception: KeycloakError, operation: str) -> None:
89+
def _handle_keycloak_exception(cls, exception: KeycloakError, operation: str) -> NoReturn:
9090
"""Handle Keycloak exceptions and map them to appropriate application errors.
9191
9292
Args:
@@ -1413,6 +1413,22 @@ def get_realm(self, realm_name: str) -> dict[str, Any] | None:
14131413
except KeycloakError as e:
14141414
self._handle_keycloak_exception(e, "get_realm")
14151415

1416+
@override
1417+
def update_realm(self, realm_name: str, **kwargs: Any) -> dict[str, Any] | None:
1418+
"""Update a realm. Kwargs are RealmRepresentation.
1419+
1420+
Args:
1421+
realm_name: Realm name (not the realm id).
1422+
**kwargs: RealmRepresentation attributes to update (e.g. displayName).
1423+
1424+
Returns:
1425+
Response from Keycloak, or None on error (handled via exception).
1426+
"""
1427+
try:
1428+
return self.admin_adapter.update_realm(realm_name, dict(kwargs))
1429+
except KeycloakError as e:
1430+
self._handle_keycloak_exception(e, "update_realm")
1431+
14161432
@override
14171433
def create_client(
14181434
self,
@@ -1582,6 +1598,101 @@ def get_composite_realm_roles(self, role_name: str) -> list[dict[str, Any]] | No
15821598
except KeycloakError as e:
15831599
self._handle_keycloak_exception(e, "get_composite_realm_roles")
15841600

1601+
@override
1602+
def get_organizations(self, query: dict | None = None) -> list[dict[str, Any]]:
1603+
"""Fetch all organizations, optionally filtered by query parameters."""
1604+
try:
1605+
return self.admin_adapter.get_organizations(query=query)
1606+
except KeycloakError as e:
1607+
self._handle_keycloak_exception(e, "get_organizations")
1608+
1609+
@override
1610+
def get_organization(self, organization_id: str) -> dict[str, Any]:
1611+
"""Get representation of the organization by ID."""
1612+
try:
1613+
return self.admin_adapter.get_organization(organization_id)
1614+
except KeycloakError as e:
1615+
self._handle_keycloak_exception(e, "get_organization")
1616+
1617+
@override
1618+
def create_organization(self, name: str, alias: str, **kwargs: Any) -> str | None:
1619+
"""Create a new organization. Name and alias must be unique. Returns org_id."""
1620+
try:
1621+
payload = {"name": name, "alias": alias}
1622+
for key, value in kwargs.items():
1623+
if key in ["name", "alias"]:
1624+
continue
1625+
1626+
# Convert snake_case to camelCase
1627+
camel_key = StringUtils.snake_to_camel_case(key)
1628+
payload[camel_key] = value
1629+
1630+
return self.admin_adapter.create_organization(payload=payload)
1631+
except KeycloakError as e:
1632+
self._handle_keycloak_exception(e, "create_organization")
1633+
1634+
@override
1635+
def update_organization(self, organization_id: str, **kwargs: Any) -> dict[str, Any]:
1636+
"""Update an existing organization. Kwargs are organization attributes (e.g. name, alias)."""
1637+
try:
1638+
payload = {}
1639+
for key, value in kwargs.items():
1640+
# Convert snake_case to camelCase
1641+
camel_key = StringUtils.snake_to_camel_case(key)
1642+
payload[camel_key] = value
1643+
1644+
return self.admin_adapter.update_organization(organization_id=organization_id, payload=payload)
1645+
except KeycloakError as e:
1646+
self._handle_keycloak_exception(e, "update_organization")
1647+
1648+
@override
1649+
def delete_organization(self, organization_id: str) -> dict[str, Any]:
1650+
"""Delete an organization."""
1651+
try:
1652+
return self.admin_adapter.delete_organization(organization_id=organization_id)
1653+
except KeycloakError as e:
1654+
self._handle_keycloak_exception(e, "delete_organization")
1655+
1656+
@override
1657+
def get_user_organizations(self, user_id: str) -> list[dict[str, Any]]:
1658+
"""Get organizations by user id."""
1659+
try:
1660+
return self.admin_adapter.get_user_organizations(user_id=user_id)
1661+
except KeycloakError as e:
1662+
self._handle_keycloak_exception(e, "get_user_organizations")
1663+
1664+
@override
1665+
def get_organization_members(self, organization_id: str, query: dict | None = None) -> list[dict[str, Any]]:
1666+
"""Get members by organization id, optionally filtered by query parameters."""
1667+
try:
1668+
return self.admin_adapter.get_organization_members(organization_id=organization_id, query=query)
1669+
except KeycloakError as e:
1670+
self._handle_keycloak_exception(e, "get_organization_members")
1671+
1672+
@override
1673+
def get_organization_members_count(self, organization_id: str) -> int:
1674+
"""Get the number of members in the organization."""
1675+
try:
1676+
return self.admin_adapter.get_organization_members_count(organization_id=organization_id)
1677+
except KeycloakError as e:
1678+
self._handle_keycloak_exception(e, "get_organization_members_count")
1679+
1680+
@override
1681+
def organization_user_add(self, user_id: str, organization_id: str) -> bytes:
1682+
"""Add a user to an organization."""
1683+
try:
1684+
return self.admin_adapter.organization_user_add(user_id=user_id, organization_id=organization_id)
1685+
except KeycloakError as e:
1686+
self._handle_keycloak_exception(e, "organization_user_add")
1687+
1688+
@override
1689+
def organization_user_remove(self, user_id: str, organization_id: str) -> dict[str, Any]:
1690+
"""Remove a user from an organization."""
1691+
try:
1692+
return self.admin_adapter.organization_user_remove(user_id=user_id, organization_id=organization_id)
1693+
except KeycloakError as e:
1694+
self._handle_keycloak_exception(e, "organization_user_remove")
1695+
15851696

15861697
class AsyncKeycloakAdapter(AsyncKeycloakPort, KeycloakExceptionHandlerMixin):
15871698
"""Concrete implementation of the KeycloakPort interface using python-keycloak library.
@@ -2724,6 +2835,22 @@ async def get_realm(self, realm_name: str) -> dict[str, Any] | None:
27242835
except KeycloakError as e:
27252836
self._handle_keycloak_exception(e, "get_realm")
27262837

2838+
@override
2839+
async def update_realm(self, realm_name: str, **kwargs: Any) -> dict[str, Any] | None:
2840+
"""Update a realm. Kwargs are RealmRepresentation top-level attributes (e.g. displayName, organizationsEnabled).
2841+
2842+
Args:
2843+
realm_name: Realm name (not the realm id).
2844+
**kwargs: RealmRepresentation attributes to update (e.g. displayName, organizationsEnabled).
2845+
2846+
Returns:
2847+
Response from Keycloak, or None on error (handled via exception).
2848+
"""
2849+
try:
2850+
return await self.admin_adapter.a_update_realm(realm_name, dict(kwargs))
2851+
except KeycloakError as e:
2852+
self._handle_keycloak_exception(e, "update_realm")
2853+
27272854
@override
27282855
async def create_client(
27292856
self,
@@ -2898,3 +3025,98 @@ async def get_composite_realm_roles(self, role_name: str) -> list[dict[str, Any]
28983025
return await self.admin_adapter.a_get_composite_realm_roles_of_role(role_name)
28993026
except KeycloakError as e:
29003027
self._handle_keycloak_exception(e, "get_composite_realm_roles")
3028+
3029+
@override
3030+
async def get_organizations(self, query: dict | None = None) -> list[dict[str, Any]]:
3031+
"""Fetch all organizations, optionally filtered by query parameters."""
3032+
try:
3033+
return await self.admin_adapter.a_get_organizations(query=query)
3034+
except KeycloakError as e:
3035+
self._handle_keycloak_exception(e, "get_organizations")
3036+
3037+
@override
3038+
async def get_organization(self, organization_id: str) -> dict[str, Any]:
3039+
"""Get representation of the organization by ID."""
3040+
try:
3041+
return await self.admin_adapter.a_get_organization(organization_id=organization_id)
3042+
except KeycloakError as e:
3043+
self._handle_keycloak_exception(e, "get_organization")
3044+
3045+
@override
3046+
async def create_organization(self, name: str, alias: str, **kwargs: Any) -> str | None:
3047+
"""Create a new organization. Name and alias must be unique. Returns org_id."""
3048+
try:
3049+
payload = {"name": name, "alias": alias}
3050+
for key, value in kwargs.items():
3051+
if key in ["name", "alias"]:
3052+
continue
3053+
3054+
# Convert snake_case to camelCase
3055+
camel_key = StringUtils.snake_to_camel_case(key)
3056+
payload[camel_key] = value
3057+
3058+
return await self.admin_adapter.a_create_organization(payload=payload)
3059+
except KeycloakError as e:
3060+
self._handle_keycloak_exception(e, "create_organization")
3061+
3062+
@override
3063+
async def update_organization(self, organization_id: str, **kwargs: Any) -> dict[str, Any]:
3064+
"""Update an existing organization. Kwargs are organization attributes (e.g. name, alias)."""
3065+
try:
3066+
payload = {}
3067+
for key, value in kwargs.items():
3068+
# Convert snake_case to camelCase
3069+
camel_key = StringUtils.snake_to_camel_case(key)
3070+
payload[camel_key] = value
3071+
3072+
return await self.admin_adapter.a_update_organization(organization_id=organization_id, payload=payload)
3073+
except KeycloakError as e:
3074+
self._handle_keycloak_exception(e, "update_organization")
3075+
3076+
@override
3077+
async def delete_organization(self, organization_id: str) -> dict[str, Any]:
3078+
"""Delete an organization."""
3079+
try:
3080+
return await self.admin_adapter.a_delete_organization(organization_id=organization_id)
3081+
except KeycloakError as e:
3082+
self._handle_keycloak_exception(e, "delete_organization")
3083+
3084+
@override
3085+
async def get_user_organizations(self, user_id: str) -> list[dict[str, Any]]:
3086+
"""Get organizations by user id."""
3087+
try:
3088+
return await self.admin_adapter.a_get_user_organizations(user_id=user_id)
3089+
except KeycloakError as e:
3090+
self._handle_keycloak_exception(e, "get_user_organizations")
3091+
3092+
@override
3093+
async def get_organization_members(self, organization_id: str, query: dict | None = None) -> list[dict[str, Any]]:
3094+
"""Get members by organization id, optionally filtered by query parameters."""
3095+
try:
3096+
return await self.admin_adapter.a_get_organization_members(organization_id=organization_id, query=query)
3097+
except KeycloakError as e:
3098+
self._handle_keycloak_exception(e, "get_organization_members")
3099+
3100+
@override
3101+
async def get_organization_members_count(self, organization_id: str) -> int:
3102+
"""Get the number of members in the organization."""
3103+
try:
3104+
return await self.admin_adapter.a_get_organization_members_count(organization_id=organization_id)
3105+
except KeycloakError as e:
3106+
self._handle_keycloak_exception(e, "get_organization_members_count")
3107+
3108+
@override
3109+
async def organization_user_add(self, user_id: str, organization_id: str) -> bytes:
3110+
"""Add a user to an organization."""
3111+
try:
3112+
return await self.admin_adapter.a_organization_user_add(user_id=user_id, organization_id=organization_id)
3113+
except KeycloakError as e:
3114+
self._handle_keycloak_exception(e, "organization_user_add")
3115+
3116+
@override
3117+
async def organization_user_remove(self, user_id: str, organization_id: str) -> dict[str, Any]:
3118+
"""Remove a user from an organization."""
3119+
try:
3120+
return await self.admin_adapter.a_organization_user_remove(user_id=user_id, organization_id=organization_id)
3121+
except KeycloakError as e:
3122+
self._handle_keycloak_exception(e, "organization_user_remove")

0 commit comments

Comments
 (0)