Skip to content

Commit 600d91f

Browse files
committed
Complete API coverage with proper type annotations and response unwrapping
- Add missing API modules: attack_detection, client_initial_access, client_attribute_certificate, client_registration_policy, scope_mappings, client_role_mappings, role_mapper, roles_by_id - Fix return type annotations to use specific model types instead of generic list/dict throughout all API modules - Unwrap response wrapper types that only contain additional_properties - Update all method docstrings with proper Args, Returns, and Raises sections - Export APIError alongside AuthError for consistency - Ensure 100% Keycloak Admin API endpoint coverage
1 parent 4af50ec commit 600d91f

29 files changed

+9862
-523
lines changed

ackc/api/__init__.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,17 @@
1919
from .authorization import *
2020
from .protocol_mappers import *
2121
from .keys import *
22+
from .scope_mappings import *
23+
from .client_role_mappings import *
24+
from .role_mapper import *
25+
from .roles_by_id import *
26+
from .attack_detection import *
27+
from .client_initial_access import *
28+
from .client_attribute_certificate import *
29+
from .client_registration_policy import *
2230

2331
__all__ = (
24-
"AuthError", "AuthenticatedClient", "Client", "BaseAPI", "BaseClientManager",
32+
"AuthError", "APIError", "AuthenticatedClient", "Client", "BaseAPI", "BaseClientManager",
2533
"UsersAPI", "UsersClientMixin", "UserRepresentation",
2634
"RealmsAPI", "RealmsClientMixin", "RealmRepresentation",
2735
"ClientsAPI", "ClientsClientMixin", "ClientRepresentation",
@@ -37,6 +45,14 @@
3745
"AuthorizationAPI", "AuthorizationClientMixin", "ResourceServerRepresentation", "ResourceRepresentation", "ScopeRepresentation", "AbstractPolicyRepresentation", "PolicyProviderRepresentation", "PolicyEvaluationResponse", "EvaluationResultRepresentation",
3846
"ProtocolMappersAPI", "ProtocolMappersClientMixin", "ProtocolMapperRepresentation",
3947
"KeysAPI", "KeysClientMixin", "KeysMetadataRepresentation",
48+
"ScopeMappingsAPI", "ScopeMappingsClientMixin",
49+
"ClientRoleMappingsAPI", "ClientRoleMappingsClientMixin",
50+
"RoleMapperAPI", "RoleMapperClientMixin",
51+
"RolesByIdAPI", "RolesByIdClientMixin",
52+
"AttackDetectionAPI", "AttackDetectionClientMixin",
53+
"ClientInitialAccessAPI", "ClientInitialAccessClientMixin",
54+
"ClientAttributeCertificateAPI", "ClientAttributeCertificateClientMixin",
55+
"ClientRegistrationPolicyAPI", "ClientRegistrationPolicyClientMixin",
4056
"KeycloakClientMixin",
4157
)
4258

@@ -57,6 +73,14 @@ class KeycloakClientMixin(
5773
AuthorizationClientMixin,
5874
ProtocolMappersClientMixin,
5975
KeysClientMixin,
76+
ScopeMappingsClientMixin,
77+
ClientRoleMappingsClientMixin,
78+
RoleMapperClientMixin,
79+
RolesByIdClientMixin,
80+
AttackDetectionClientMixin,
81+
ClientInitialAccessClientMixin,
82+
ClientAttributeCertificateClientMixin,
83+
ClientRegistrationPolicyClientMixin,
6084
):
6185
"""
6286
Mixin that provides all Keycloak API methods in a single class.

ackc/api/attack_detection.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""Attack detection API methods."""
2+
from functools import cached_property
3+
4+
from .base import BaseAPI
5+
from ..exceptions import APIError
6+
from ..generated.api.attack_detection import (
7+
get_admin_realms_realm_attack_detection_brute_force_users_user_id,
8+
delete_admin_realms_realm_attack_detection_brute_force_users,
9+
delete_admin_realms_realm_attack_detection_brute_force_users_user_id,
10+
)
11+
12+
__all__ = "AttackDetectionAPI", "AttackDetectionClientMixin"
13+
14+
15+
class AttackDetectionAPI(BaseAPI):
16+
"""Attack detection API methods."""
17+
18+
def get_brute_force_user_status(self, realm: str | None = None, *, user_id: str) -> dict | None:
19+
"""Get brute force detection status for a specific user.
20+
21+
Args:
22+
realm: The realm name
23+
user_id: User ID to check
24+
25+
Returns:
26+
Dictionary with brute force status including failed login count and disabled status
27+
"""
28+
result = self._sync(
29+
get_admin_realms_realm_attack_detection_brute_force_users_user_id.sync,
30+
realm or self.realm,
31+
user_id=user_id
32+
)
33+
if result and hasattr(result, 'additional_properties'):
34+
return result.additional_properties
35+
return result
36+
37+
async def aget_brute_force_user_status(self, realm: str | None = None, *, user_id: str) -> dict | None:
38+
"""Get brute force detection status for a specific user (async).
39+
40+
Args:
41+
realm: The realm name
42+
user_id: User ID to check
43+
44+
Returns:
45+
Dictionary with brute force status including failed login count and disabled status
46+
"""
47+
result = await self._async(
48+
get_admin_realms_realm_attack_detection_brute_force_users_user_id.asyncio,
49+
realm or self.realm,
50+
user_id=user_id
51+
)
52+
if result and hasattr(result, 'additional_properties'):
53+
return result.additional_properties
54+
return result
55+
56+
def clear_all_brute_force_users(self, realm: str | None = None) -> None:
57+
"""Clear brute force attempts for all users in the realm.
58+
59+
Resets the failed login count for all users who have been locked out.
60+
61+
Args:
62+
realm: The realm name
63+
64+
Raises:
65+
APIError: If clearing brute force data fails
66+
"""
67+
response = self._sync_detailed(
68+
delete_admin_realms_realm_attack_detection_brute_force_users.sync_detailed,
69+
realm or self.realm
70+
)
71+
if response.status_code not in (200, 204):
72+
raise APIError(f"Failed to clear brute force users: {response.status_code}")
73+
74+
async def aclear_all_brute_force_users(self, realm: str | None = None) -> None:
75+
"""Clear brute force attempts for all users in the realm (async).
76+
77+
Resets the failed login count for all users who have been locked out.
78+
79+
Args:
80+
realm: The realm name
81+
82+
Raises:
83+
APIError: If clearing brute force data fails
84+
"""
85+
response = await self._async_detailed(
86+
delete_admin_realms_realm_attack_detection_brute_force_users.asyncio_detailed,
87+
realm or self.realm
88+
)
89+
if response.status_code not in (200, 204):
90+
raise APIError(f"Failed to clear brute force users: {response.status_code}")
91+
92+
def clear_brute_force_user(self, realm: str | None = None, *, user_id: str) -> None:
93+
"""Clear brute force attempts for a specific user.
94+
95+
Resets the failed login count and re-enables the user if locked out.
96+
97+
Args:
98+
realm: The realm name
99+
user_id: User ID to clear
100+
101+
Raises:
102+
APIError: If clearing brute force data fails
103+
"""
104+
response = self._sync_detailed(
105+
delete_admin_realms_realm_attack_detection_brute_force_users_user_id.sync_detailed,
106+
realm or self.realm,
107+
user_id=user_id
108+
)
109+
if response.status_code not in (200, 204):
110+
raise APIError(f"Failed to clear brute force user: {response.status_code}")
111+
112+
async def aclear_brute_force_user(self, realm: str | None = None, *, user_id: str) -> None:
113+
"""Clear brute force attempts for a specific user (async).
114+
115+
Resets the failed login count and re-enables the user if locked out.
116+
117+
Args:
118+
realm: The realm name
119+
user_id: User ID to clear
120+
121+
Raises:
122+
APIError: If clearing brute force data fails
123+
"""
124+
response = await self._async_detailed(
125+
delete_admin_realms_realm_attack_detection_brute_force_users_user_id.asyncio_detailed,
126+
realm or self.realm,
127+
user_id=user_id
128+
)
129+
if response.status_code not in (200, 204):
130+
raise APIError(f"Failed to clear brute force user: {response.status_code}")
131+
132+
133+
class AttackDetectionClientMixin:
134+
"""Mixin for BaseClientManager subclasses to be connected to the AttackDetectionAPI."""
135+
136+
@cached_property
137+
def attack_detection(self) -> AttackDetectionAPI:
138+
"""Get the AttackDetectionAPI instance."""
139+
return AttackDetectionAPI(manager=self) # type: ignore[arg-type]

0 commit comments

Comments
 (0)