|
1 | 1 | import json |
2 | 2 | import logging |
3 | 3 | import time |
4 | | -from typing import Any, override |
| 4 | +from typing import Any, NoReturn, override |
5 | 5 |
|
6 | 6 | from async_lru import alru_cache |
7 | 7 | from keycloak import KeycloakAdmin, KeycloakOpenID |
@@ -86,7 +86,7 @@ def _extract_error_message(cls, exception: KeycloakError) -> str: |
86 | 86 | return error_message |
87 | 87 |
|
88 | 88 | @classmethod |
89 | | - def _handle_keycloak_exception(cls, exception: KeycloakError, operation: str) -> None: |
| 89 | + def _handle_keycloak_exception(cls, exception: KeycloakError, operation: str) -> NoReturn: |
90 | 90 | """Handle Keycloak exceptions and map them to appropriate application errors. |
91 | 91 |
|
92 | 92 | Args: |
@@ -1413,6 +1413,22 @@ def get_realm(self, realm_name: str) -> dict[str, Any] | None: |
1413 | 1413 | except KeycloakError as e: |
1414 | 1414 | self._handle_keycloak_exception(e, "get_realm") |
1415 | 1415 |
|
| 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 | + |
1416 | 1432 | @override |
1417 | 1433 | def create_client( |
1418 | 1434 | self, |
@@ -1582,6 +1598,101 @@ def get_composite_realm_roles(self, role_name: str) -> list[dict[str, Any]] | No |
1582 | 1598 | except KeycloakError as e: |
1583 | 1599 | self._handle_keycloak_exception(e, "get_composite_realm_roles") |
1584 | 1600 |
|
| 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 | + |
1585 | 1696 |
|
1586 | 1697 | class AsyncKeycloakAdapter(AsyncKeycloakPort, KeycloakExceptionHandlerMixin): |
1587 | 1698 | """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: |
2724 | 2835 | except KeycloakError as e: |
2725 | 2836 | self._handle_keycloak_exception(e, "get_realm") |
2726 | 2837 |
|
| 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 | + |
2727 | 2854 | @override |
2728 | 2855 | async def create_client( |
2729 | 2856 | self, |
@@ -2898,3 +3025,98 @@ async def get_composite_realm_roles(self, role_name: str) -> list[dict[str, Any] |
2898 | 3025 | return await self.admin_adapter.a_get_composite_realm_roles_of_role(role_name) |
2899 | 3026 | except KeycloakError as e: |
2900 | 3027 | 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