Skip to content

Commit 5160bee

Browse files
committed
feat: Add Management API support to registry client and CLI
Extend registry_client.py and registry_management.py to support the new Management API endpoints from PR #267 for IAM/user management. Changes: - Add 6 Pydantic models for Management API requests/responses - Add 5 client methods: list_users, create_m2m_account, create_human_user, delete_user, list_keycloak_iam_groups - Add 5 CLI commands: user-list, user-create-m2m, user-create-human, user-delete, keycloak-groups - Update documentation with usage examples The Management API enables administrators to: - List and search Keycloak users - Create M2M service accounts with client credentials - Create human user accounts with group assignments - Delete users with confirmation prompts - List raw Keycloak IAM groups All commands require admin privileges (enforced server-side).
1 parent 6dc37a6 commit 5160bee

File tree

3 files changed

+554
-1
lines changed

3 files changed

+554
-1
lines changed

api/registry_client.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,62 @@ class AnthropicErrorResponse(BaseModel):
545545
error: str = Field(..., description="Error message")
546546

547547

548+
# Management API Models (IAM/User Management)
549+
550+
551+
class M2MAccountRequest(BaseModel):
552+
"""Request model for creating M2M service account."""
553+
554+
name: str = Field(..., min_length=1, description="Service account name/client ID")
555+
groups: List[str] = Field(..., min_length=1, description="List of group names")
556+
description: Optional[str] = Field(None, description="Account description")
557+
558+
559+
class HumanUserRequest(BaseModel):
560+
"""Request model for creating human user account."""
561+
562+
username: str = Field(..., min_length=1, description="Username")
563+
email: str = Field(..., description="Email address")
564+
first_name: str = Field(..., min_length=1, description="First name")
565+
last_name: str = Field(..., min_length=1, description="Last name")
566+
groups: List[str] = Field(..., min_length=1, description="List of group names")
567+
password: Optional[str] = Field(None, description="Initial password")
568+
569+
570+
class KeycloakUserSummary(BaseModel):
571+
"""Keycloak user summary model."""
572+
573+
id: str = Field(..., description="User ID")
574+
username: str = Field(..., description="Username")
575+
email: Optional[str] = Field(None, description="Email address")
576+
firstName: Optional[str] = Field(None, description="First name")
577+
lastName: Optional[str] = Field(None, description="Last name")
578+
enabled: bool = Field(True, description="Whether user is enabled")
579+
groups: List[str] = Field(default_factory=list, description="User groups")
580+
581+
582+
class UserListResponse(BaseModel):
583+
"""Response model for list users endpoint."""
584+
585+
users: List[KeycloakUserSummary] = Field(default_factory=list, description="List of users")
586+
total: int = Field(..., description="Total number of users")
587+
588+
589+
class UserDeleteResponse(BaseModel):
590+
"""Response model for delete user endpoint."""
591+
592+
username: str = Field(..., description="Deleted username")
593+
deleted: bool = Field(True, description="Deletion status")
594+
595+
596+
class M2MAccountResponse(BaseModel):
597+
"""Response model for M2M account creation."""
598+
599+
client_id: str = Field(..., description="Client ID")
600+
client_secret: str = Field(..., description="Client secret")
601+
groups: List[str] = Field(default_factory=list, description="Assigned groups")
602+
603+
548604
class RegistryClient:
549605
"""
550606
MCP Gateway Registry API client.
@@ -553,6 +609,7 @@ class RegistryClient:
553609
- Server Management: registration, removal, toggling, health checks
554610
- Group Management: create, delete, list groups
555611
- Agent Management: register, update, delete, discover agents (A2A)
612+
- Management API: IAM/user management, M2M accounts, user CRUD operations
556613
557614
Authentication is handled via JWT tokens passed to the constructor.
558615
"""
@@ -1374,3 +1431,185 @@ def anthropic_get_server_version(
13741431
result = AnthropicServerResponse(**response.json())
13751432
logger.info(f"Retrieved server details for {server_name} v{version}")
13761433
return result
1434+
1435+
1436+
# Management API Methods (IAM/User Management)
1437+
1438+
1439+
def list_users(
1440+
self,
1441+
search: Optional[str] = None,
1442+
limit: int = 500
1443+
) -> UserListResponse:
1444+
"""
1445+
List Keycloak users (admin only).
1446+
1447+
Args:
1448+
search: Optional search string to filter users
1449+
limit: Maximum number of results (default: 500)
1450+
1451+
Returns:
1452+
UserListResponse with list of users
1453+
1454+
Raises:
1455+
requests.HTTPError: If not authorized (403) or request fails
1456+
"""
1457+
logger.info("Listing Keycloak users")
1458+
1459+
params = {}
1460+
if search:
1461+
params["search"] = search
1462+
if limit != 500:
1463+
params["limit"] = limit
1464+
1465+
response = self._make_request(
1466+
method="GET",
1467+
endpoint="/management/iam/users",
1468+
params=params
1469+
)
1470+
1471+
result = UserListResponse(**response.json())
1472+
logger.info(f"Retrieved {result.total} users")
1473+
return result
1474+
1475+
1476+
def create_m2m_account(
1477+
self,
1478+
name: str,
1479+
groups: List[str],
1480+
description: Optional[str] = None
1481+
) -> M2MAccountResponse:
1482+
"""
1483+
Create a machine-to-machine service account.
1484+
1485+
Args:
1486+
name: Service account name/client ID
1487+
groups: List of group names for access control
1488+
description: Optional account description
1489+
1490+
Returns:
1491+
M2MAccountResponse with client credentials
1492+
1493+
Raises:
1494+
requests.HTTPError: If not authorized (403), already exists (400), or request fails
1495+
"""
1496+
logger.info(f"Creating M2M service account: {name}")
1497+
1498+
data = {
1499+
"name": name,
1500+
"groups": groups
1501+
}
1502+
if description:
1503+
data["description"] = description
1504+
1505+
response = self._make_request(
1506+
method="POST",
1507+
endpoint="/management/iam/users/m2m",
1508+
data=data
1509+
)
1510+
1511+
result = M2MAccountResponse(**response.json())
1512+
logger.info(f"M2M account created successfully: {name}")
1513+
return result
1514+
1515+
1516+
def create_human_user(
1517+
self,
1518+
username: str,
1519+
email: str,
1520+
first_name: str,
1521+
last_name: str,
1522+
groups: List[str],
1523+
password: Optional[str] = None
1524+
) -> KeycloakUserSummary:
1525+
"""
1526+
Create a human user account in Keycloak.
1527+
1528+
Args:
1529+
username: Username
1530+
email: Email address
1531+
first_name: First name
1532+
last_name: Last name
1533+
groups: List of group names
1534+
password: Optional initial password
1535+
1536+
Returns:
1537+
KeycloakUserSummary with created user details
1538+
1539+
Raises:
1540+
requests.HTTPError: If not authorized (403), already exists (400), or request fails
1541+
"""
1542+
logger.info(f"Creating human user: {username}")
1543+
1544+
data = {
1545+
"username": username,
1546+
"email": email,
1547+
"firstname": first_name,
1548+
"lastname": last_name,
1549+
"groups": groups
1550+
}
1551+
if password:
1552+
data["password"] = password
1553+
1554+
response = self._make_request(
1555+
method="POST",
1556+
endpoint="/management/iam/users/human",
1557+
data=data
1558+
)
1559+
1560+
result = KeycloakUserSummary(**response.json())
1561+
logger.info(f"User created successfully: {username}")
1562+
return result
1563+
1564+
1565+
def delete_user(
1566+
self,
1567+
username: str
1568+
) -> UserDeleteResponse:
1569+
"""
1570+
Delete a user by username.
1571+
1572+
Args:
1573+
username: Username to delete
1574+
1575+
Returns:
1576+
UserDeleteResponse confirming deletion
1577+
1578+
Raises:
1579+
requests.HTTPError: If not authorized (403), not found (400/404), or request fails
1580+
"""
1581+
logger.info(f"Deleting user: {username}")
1582+
1583+
response = self._make_request(
1584+
method="DELETE",
1585+
endpoint=f"/management/iam/users/{username}"
1586+
)
1587+
1588+
result = UserDeleteResponse(**response.json())
1589+
logger.info(f"User deleted successfully: {username}")
1590+
return result
1591+
1592+
1593+
def list_keycloak_iam_groups(self) -> List[Dict[str, Any]]:
1594+
"""
1595+
List Keycloak IAM groups (raw Keycloak data).
1596+
1597+
This is different from list_groups() which returns groups with server associations.
1598+
This method returns raw Keycloak group data without scopes.
1599+
1600+
Returns:
1601+
List of Keycloak group dictionaries
1602+
1603+
Raises:
1604+
requests.HTTPError: If not authorized (403) or request fails
1605+
"""
1606+
logger.info("Listing Keycloak IAM groups")
1607+
1608+
response = self._make_request(
1609+
method="GET",
1610+
endpoint="/management/iam/groups"
1611+
)
1612+
1613+
result = response.json()
1614+
logger.info(f"Retrieved {len(result)} Keycloak groups")
1615+
return result

0 commit comments

Comments
 (0)