feat(mcp): add users and roles tools to the Prowler MCP Server (#12088)

This commit is contained in:
Rubén De la Torre Vico
2026-07-28 17:16:56 +02:00
committed by GitHub
parent 7ba96ab2e2
commit 8eeb37aea4
11 changed files with 793 additions and 3 deletions
@@ -10,7 +10,7 @@ Complete reference guide for all tools available in the Prowler MCP Server. Tool
|----------|------------|------------------------|
| Prowler Hub | 10 tools | No |
| Prowler Documentation | 2 tools | No |
| Prowler Cloud, Private Cloud & Local Server | 32 tools | Yes |
| Prowler Cloud, Private Cloud & Local Server | 39 tools | Yes |
## Tool Naming Convention
@@ -105,6 +105,23 @@ Tools for viewing compliance status and framework details across all cloud provi
- **`prowler_get_compliance_overview`** - Get high-level compliance status across all frameworks for a specific scan or provider, including pass/fail statistics per framework
- **`prowler_get_compliance_framework_state_details`** - Get detailed requirement-level breakdown for a specific compliance framework, including failed requirements and associated finding IDs
### User Management
Tools for viewing the users in your tenant and identifying the authenticated user.
- **`prowler_list_users`** - List the users in the tenant with their names and emails
- **`prowler_get_user`** - Get detailed information about a specific user by ID, including join date and role/membership IDs
- **`prowler_get_current_user`** - Identify which user the current credentials authenticate as
### Role Management
Tools for browsing RBAC roles and managing the role assigned to a user. A user holds exactly one role, so setting a role replaces the one they held before.
- **`prowler_list_roles`** - List the roles defined in the tenant with their permission scope
- **`prowler_get_role`** - Get detailed information about a specific role by ID, including granted capabilities, visibility scope, assigned users, and provider groups
- **`prowler_get_user_roles`** - List the roles assigned to a specific user, with the capabilities each role grants
- **`prowler_set_user_role`** - Set the role a user holds, replacing the role they had before (idempotent)
## Prowler Hub Tools
Access Prowler's security check catalog and compliance frameworks. **No authentication required.**
@@ -50,6 +50,7 @@ Full access to your Prowler deployment — Prowler Cloud, Prowler Private Cloud,
- **Resource Inventory**: Search and view detailed information about your audited resources
- **Muting Management**: Create and manage muting lists/rules to suppress non-relevant findings
- **Attack Paths Analysis**: Analyze privilege escalation chains and security misconfigurations through graph-based analysis of cloud resource relationships
- **User & Role Management**: List the users in your tenant, identify the authenticated user, browse RBAC roles, and set the role a user holds
### 2. Prowler Hub
+1
View File
@@ -16,6 +16,7 @@ Full access to your Prowler data (Prowler Cloud, Prowler Private Cloud, or Prowl
- **Resource Inventory**: Search and view detailed information about your audited resources
- **Muting Management**: Create and manage muting rules to suppress non-critical findings
- **Compliance Reporting**: View compliance status across frameworks and drill into requirement-level details
- **User & Role Management**: List the users in your tenant, identify the authenticated user, browse RBAC roles, and set the role a user holds
### Prowler Hub
@@ -0,0 +1 @@
RBAC role tools `prowler_list_roles`, `prowler_get_role`, `prowler_get_user_roles`, and `prowler_set_user_role` for browsing roles and setting the role a user holds
@@ -0,0 +1 @@
Read-only user management tools `prowler_list_users`, `prowler_get_user`, and `prowler_get_current_user` for listing tenant users with their emails and identifying the authenticated user
@@ -0,0 +1,230 @@
"""Data models for Prowler RBAC roles.
This module provides Pydantic models for representing Prowler roles with
two-tier complexity:
- SimplifiedRole: For list operations with essential identification fields
- DetailedRole: Extends simplified with the capabilities the role grants and
its related users / provider groups
It also provides UserRolesResult, used by the tools that read or change the
roles assigned to a specific user.
All models inherit from MinimalSerializerMixin to exclude None/empty values
for optimal LLM token usage.
"""
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
from prowler_mcp_server.prowler_app.models.utils import extract_relationship_ids
# Role capabilities are exposed by the API as boolean "manage_*" attributes.
_PERMISSION_ATTRIBUTE_PREFIX = "manage_"
class SimplifiedRole(MinimalSerializerMixin, BaseModel):
"""Simplified role representation for list operations.
Includes core identification fields for efficient overview.
Used by list_roles() tool.
"""
model_config = ConfigDict(frozen=True)
id: str = Field(
description="Unique UUIDv4 identifier for this role in Prowler database"
)
name: str = Field(description="Human-readable name of the role")
permission_state: str | None = Field(
default=None,
description="Summary of the role's permissions: 'unlimited' (all), 'limited' (some), or 'none'",
)
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "SimplifiedRole":
"""Transform a JSON:API role resource into a simplified model.
Args:
data: Role data from API response['data'] (single item or list item)
Returns:
SimplifiedRole instance
"""
attributes = data["attributes"]
return cls(
id=data["id"],
name=attributes["name"],
permission_state=attributes.get("permission_state"),
)
class DetailedRole(SimplifiedRole):
"""Detailed role representation with granted capabilities and relationships.
Extends SimplifiedRole with the concrete management capabilities the role
grants, its visibility scope, and the IDs of related users and provider
groups. Used by get_role(), get_user_roles() and set_user_role().
"""
model_config = ConfigDict(frozen=True)
permissions: list[str] | None = Field(
default=None,
description="Management capabilities granted by this role, as reported by the API (only the enabled ones), e.g. ['manage_users', 'manage_scans']. Deployments do not all expose the same capabilities, so read `permission_state` for the authoritative summary: 'unlimited' means the role grants every capability, including any not listed here.",
)
unlimited_visibility: bool | None = Field(
default=None,
description="Whether the role can see all providers (True) or only those in its provider groups (False)",
)
provider_group_ids: list[str] | None = Field(
default=None,
description="UUIDv4 identifiers of the provider groups this role is scoped to. An empty list means the role is not scoped to any provider group.",
)
user_ids: list[str] | None = Field(
default=None,
description="UUIDv4 identifiers of the users this role is assigned to. An empty list means the role is not assigned to any user.",
)
inserted_at: str | None = Field(
default=None, description="ISO 8601 timestamp when the role was created"
)
updated_at: str | None = Field(
default=None, description="ISO 8601 timestamp when the role was last modified"
)
def _should_exclude(self, key: str, value: Any) -> bool:
"""Keep fields whose "empty" form carries meaning.
``unlimited_visibility`` is kept even when ``False``, and ``permissions``
and the relationship lists are kept even when empty so that an empty
``permissions``/``user_ids``/``provider_group_ids`` explicitly signals
"grants no capabilities / not assigned to any user / not scoped to any
provider group" instead of looking like an omitted, unknown field to an
agent.
"""
if key in (
"unlimited_visibility",
"permissions",
"user_ids",
"provider_group_ids",
):
return value is None
return super()._should_exclude(key, value)
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "DetailedRole":
"""Transform a JSON:API role resource into a detailed model.
Args:
data: Role data from API response['data'] or an included role
Returns:
DetailedRole instance with all fields populated
"""
attributes = data["attributes"]
relationships = data.get("relationships", {})
permissions = [
name
for name, enabled in attributes.items()
if name.startswith(_PERMISSION_ATTRIBUTE_PREFIX) and enabled
]
return cls(
id=data["id"],
name=attributes["name"],
permission_state=attributes.get("permission_state"),
permissions=permissions,
unlimited_visibility=attributes.get("unlimited_visibility"),
provider_group_ids=extract_relationship_ids(
relationships, "provider_groups"
),
user_ids=extract_relationship_ids(relationships, "users"),
inserted_at=attributes.get("inserted_at"),
updated_at=attributes.get("updated_at"),
)
class RolesListResponse(BaseModel):
"""Response model for list_roles() with pagination metadata.
Follows the established pattern from ScansListResponse and UsersListResponse.
"""
roles: list[SimplifiedRole]
total_num_roles: int
total_num_pages: int
current_page: int
@classmethod
def from_api_response(cls, response: dict[str, Any]) -> "RolesListResponse":
"""Transform a JSON:API list response into a roles list with pagination.
Args:
response: Full API response with data and meta
Returns:
RolesListResponse with simplified roles and pagination metadata
"""
data = response.get("data", [])
meta = response.get("meta", {})
pagination = meta.get("pagination", {})
roles = [SimplifiedRole.from_api_response(item) for item in data]
return cls(
roles=roles,
total_num_roles=pagination.get("count", 0),
total_num_pages=pagination.get("pages", 0),
current_page=pagination.get("page", 1),
)
class UserRolesResult(MinimalSerializerMixin, BaseModel):
"""The roles currently assigned to a user.
Used by get_user_roles() to report a user's roles, and by set_user_role()
to report the authoritative role set after a change (with `changed` and
`message` describing the outcome).
"""
user_id: str = Field(description="UUIDv4 identifier of the user")
total_num_roles: int = Field(
description="Number of roles currently assigned to the user"
)
roles: list[DetailedRole] = Field(
description="The roles currently assigned to the user, with their granted capabilities"
)
changed: bool | None = Field(
default=None,
description="For assignment operations: whether this call actually modified the user's roles",
)
message: str | None = Field(
default=None,
description="For assignment operations: human-readable description of the outcome",
)
def _should_exclude(self, key: str, value: Any) -> bool:
"""Always include the roles list, even when empty (explicit 'no roles')."""
if key == "roles":
return False
return super()._should_exclude(key, value)
@classmethod
def build(
cls,
user_id: str,
roles: list[DetailedRole],
changed: bool | None = None,
message: str | None = None,
) -> "UserRolesResult":
"""Assemble a result from a user's role list, filling the count."""
return cls(
user_id=user_id,
total_num_roles=len(roles),
roles=roles,
changed=changed,
message=message,
)
@@ -0,0 +1,143 @@
"""Data models for Prowler users.
This module provides Pydantic models for representing Prowler users with
two-tier complexity:
- SimplifiedUser: For list operations with essential identification fields
- DetailedUser: Extends simplified with account metadata and role/membership links
All models inherit from MinimalSerializerMixin to exclude None/empty values
for optimal LLM token usage.
"""
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
from prowler_mcp_server.prowler_app.models.utils import extract_relationship_ids
class SimplifiedUser(MinimalSerializerMixin, BaseModel):
"""Simplified user representation for list operations.
Includes core identification fields for efficient overview.
Used by list_users() tool.
"""
model_config = ConfigDict(frozen=True)
id: str = Field(
description="Unique UUIDv4 identifier for this user in Prowler database"
)
name: str = Field(description="Display name of the user")
email: str = Field(description="Email address of the user")
company_name: str | None = Field(
default=None, description="Company the user belongs to, if provided"
)
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "SimplifiedUser":
"""Transform a JSON:API user resource into a simplified model.
Args:
data: User data from API response['data'] (single item or list item)
Returns:
SimplifiedUser instance
"""
attributes = data["attributes"]
return cls(
id=data["id"],
name=attributes["name"],
email=attributes["email"],
company_name=attributes.get("company_name"),
)
class DetailedUser(SimplifiedUser):
"""Detailed user representation with account metadata and relationships.
Extends SimplifiedUser with the join date and the IDs of the roles and
memberships associated with the user.
Used by get_user() and get_current_user() tools.
Note: ``role_ids`` and ``membership_ids`` are omitted when empty because an
empty list is ambiguous here. The API hides another user's roles/memberships
from callers without MANAGE_ACCOUNT (returning them empty rather than
forbidden), so an empty list cannot be told apart from "genuinely none". They
are only reported when at least one ID is visible.
"""
model_config = ConfigDict(frozen=True)
date_joined: str | None = Field(
default=None,
description="ISO 8601 timestamp when the user joined",
)
role_ids: list[str] | None = Field(
default=None,
description="UUIDv4 identifiers of the roles assigned to the user (omitted when none are visible)",
)
membership_ids: list[str] | None = Field(
default=None,
description="UUIDv4 identifiers of the tenant memberships of the user (omitted when none are visible)",
)
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "DetailedUser":
"""Transform a JSON:API user resource into a detailed model.
Args:
data: User data from API response['data']
Returns:
DetailedUser instance with all fields populated
"""
attributes = data["attributes"]
relationships = data.get("relationships", {})
return cls(
id=data["id"],
name=attributes["name"],
email=attributes["email"],
company_name=attributes.get("company_name"),
date_joined=attributes.get("date_joined"),
role_ids=extract_relationship_ids(relationships, "roles"),
membership_ids=extract_relationship_ids(relationships, "memberships"),
)
class UsersListResponse(BaseModel):
"""Response model for list_users() with pagination metadata.
Follows the established pattern from ScansListResponse and ProvidersListResponse.
"""
users: list[SimplifiedUser]
total_num_users: int
total_num_pages: int
current_page: int
@classmethod
def from_api_response(cls, response: dict[str, Any]) -> "UsersListResponse":
"""Transform a JSON:API list response into a users list with pagination.
Args:
response: Full API response with data and meta
Returns:
UsersListResponse with simplified users and pagination metadata
"""
data = response.get("data", [])
meta = response.get("meta", {})
pagination = meta.get("pagination", {})
users = [SimplifiedUser.from_api_response(item) for item in data]
return cls(
users=users,
total_num_users=pagination.get("count", 0),
total_num_pages=pagination.get("pages", 0),
current_page=pagination.get("page", 1),
)
@@ -0,0 +1,45 @@
"""Shared helpers for building models from Prowler API responses.
Stateless utilities used by the models' ``from_api_response()`` factory methods
to read the JSON:API document structure (relationships, linkage, etc.). Keeping
them here leaves ``base.py`` focused on the base model/mixin and gives these
response-parsing helpers a single, discoverable home.
"""
from typing import Any
def extract_relationship_ids(
relationships: dict[str, Any], relationship_name: str
) -> list[str] | None:
"""Extract related resource IDs from a JSON:API relationship.
Handles both to-one (``data`` is an object) and to-many (``data`` is a list)
relationships, returning a flat list of IDs in either case.
The absent and present-but-empty cases are deliberately distinguished so
callers can tell "the relationship was not part of this document" from "the
relationship is genuinely empty":
- Relationship key absent → ``None`` (unknown; the serializer did not expose
it, e.g. a role included via ``?include=roles`` carries no ``users``).
- Relationship key present but with no members → ``[]`` (explicitly none).
Args:
relationships: The ``relationships`` object from a JSON:API resource
relationship_name: The relationship key to read (e.g. ``"roles"``)
Returns:
List of related resource IDs, ``[]`` if the relationship is present but
empty, or ``None`` if the relationship is absent from the document.
"""
relationship = relationships.get(relationship_name)
if relationship is None:
return None
data = relationship.get("data")
if not data:
return []
if isinstance(data, list):
return [item["id"] for item in data if item and item.get("id")]
# to-one relationship
return [data["id"]] if data.get("id") else []
@@ -0,0 +1,222 @@
"""Role (RBAC) tools for Prowler MCP Server.
This module provides read tools for browsing roles and inspecting the role a
user holds, plus a tool for setting it.
A user holds exactly one role: the API resolves a user's permissions from a
single role (`get_role` in `api.rbac.permissions`) and the UI only ever assigns
one, so setting a role replaces the one the user currently holds instead of
adding to it.
"""
from typing import Any
from pydantic import Field
from prowler_mcp_server.prowler_app.models.roles import (
DetailedRole,
RolesListResponse,
UserRolesResult,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
class RolesTools(BaseTool):
"""Tools for RBAC role operations.
Provides tools for:
- prowler_list_roles: List the roles defined in the tenant
- prowler_get_role: Get detailed information about a specific role by ID
- prowler_get_user_roles: List the roles assigned to a specific user
- prowler_set_user_role: Set the role a user holds (idempotent)
"""
async def list_roles(
self,
page_size: int = Field(
default=50, description="Number of results to return per page"
),
page_number: int = Field(
default=1, description="Page number to retrieve (1-indexed)"
),
) -> dict[str, Any]:
"""List the RBAC roles defined in the authenticated tenant.
Use this to discover which roles exist and their permission scope before
assigning one to a user. Returns LIGHTWEIGHT role information.
Each role includes:
- id: Prowler internal UUID (v4), used with `prowler_get_role` and the assignment tools
- name: Human-readable role name
- permission_state: Summary of what the role grants ('unlimited', 'limited' or 'none')
For the concrete capabilities a role grants and the users/provider groups
it relates to, use `prowler_get_role`.
"""
self.api_client.validate_page_size(page_size)
params: dict[str, Any] = {
"fields[roles]": "name,permission_state",
"page[number]": page_number,
"page[size]": page_size,
}
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get("/roles", params=clean_params)
simplified_response = RolesListResponse.from_api_response(api_response)
return simplified_response.model_dump()
async def get_role(
self,
role_id: str = Field(
description="Prowler's internal UUID (v4) for the role to retrieve. Use `prowler_list_roles` to find role IDs if you only know a name."
),
) -> dict[str, Any]:
"""Retrieve detailed information about a specific role by its ID.
Returns everything `prowler_list_roles` returns PLUS:
- permissions: The management capabilities the role grants (only the enabled ones). Read `permission_state` for the authoritative summary: 'unlimited' means the role grants every capability, including any this deployment does not list individually
- unlimited_visibility: Whether the role can see all providers or only its provider groups
- provider_group_ids: Provider groups the role is scoped to (empty list means it is scoped to no provider group)
- user_ids: Users the role is assigned to (empty list means it is assigned to no user)
- inserted_at / updated_at: Lifecycle timestamps
The `user_ids` and `provider_group_ids` fields are always present: an
empty list means "none", not "unknown".
Workflow:
1. Use `prowler_list_roles` to browse roles and find the target role 'id'
2. Use this tool with that 'id' to inspect exactly what the role grants
"""
api_response = await self.api_client.get(f"/roles/{role_id}")
detailed_role = DetailedRole.from_api_response(api_response["data"])
return detailed_role.model_dump()
async def get_user_roles(
self,
user_id: str = Field(
description="Prowler's internal UUID (v4) for the user whose roles you want. Use `prowler_list_users` to find user IDs, or `prowler_get_current_user` for the caller."
),
) -> dict[str, Any]:
"""List the roles currently assigned to a specific user.
Returns the user's roles with the concrete capabilities each one grants,
so you can see what the user is allowed to do in the tenant. A user
normally holds a single role.
Note: this reads the user's record, so it requires MANAGE_USERS (the same
permission `prowler_get_user` needs). Each role's `user_ids` and
`provider_group_ids` are not resolved here; use `prowler_get_role` for a
role's full assignment and provider-group scope.
Workflow:
1. Use `prowler_list_users` (or `prowler_get_current_user`) to find the user 'id'
2. Use this tool to see which role they hold and what it grants
3. Use `prowler_set_user_role` to change it
"""
roles = await self._fetch_user_roles(user_id)
return UserRolesResult.build(user_id=user_id, roles=roles).model_dump()
async def set_user_role(
self,
user_id: str = Field(
description="Prowler's internal UUID (v4) for the user whose role you want to set. Use `prowler_list_users` to find user IDs."
),
role_id: str = Field(
description="Prowler's internal UUID (v4) for the role the user should hold. Use `prowler_list_roles` to find role IDs."
),
) -> dict[str, Any]:
"""Set the role a user holds, replacing the role they had before.
A user holds exactly one role in Prowler: their permissions are resolved
from a single role, so granting a new one REPLACES the previous one
instead of adding to it. To change what a user can do, set the role that
grants the capabilities they should have.
This tool is idempotent: if the user already holds only this role, it
makes no change and reports `changed: false`. It always returns the
user's up-to-date role after the operation.
Note: this operation requires both MANAGE_ACCOUNT (to change role
assignments) and MANAGE_USERS (to read the user's current role). The API
rejects the change when it would leave the tenant without a user holding
MANAGE_ACCOUNT; such rejections are surfaced as errors.
Workflow:
1. Use `prowler_list_roles` to find the role 'id' to grant
2. Use `prowler_list_users` to find the target user 'id'
3. Use this tool to set the user's role
"""
current_roles = await self._fetch_user_roles(user_id)
if [role.id for role in current_roles] == [role_id]:
return UserRolesResult.build(
user_id=user_id,
roles=current_roles,
changed=False,
message=f"User {user_id} already holds role {role_id}; no change made.",
).model_dump()
# The relationship endpoint accepts any well-formed UUID and silently
# drops role IDs that do not exist in this tenant, which would leave the
# user with no role at all. Confirm the role exists before replacing.
try:
await self.api_client.get(f"/roles/{role_id}")
except Exception as e:
raise ValueError(
f"Role {role_id} could not be read ({e}), so user {user_id} was left "
f"unchanged. Use `prowler_list_roles` to find a valid role ID."
) from e
# PATCH replaces the user's whole role set with this single role, the
# same call the Prowler UI makes when changing a user's role.
await self.api_client.patch(
f"/users/{user_id}/relationships/roles",
json_data={"data": [{"type": "roles", "id": role_id}]},
)
# After the change, fetch the user's roles again to report the authoritative state
updated_roles = await self._fetch_user_roles(user_id)
return UserRolesResult.build(
user_id=user_id,
roles=updated_roles,
changed=True,
message=f"Role {role_id} set for user {user_id}.",
).model_dump()
# Private helper methods
async def _fetch_user_roles(self, user_id: str) -> list[DetailedRole]:
"""Fetch the roles currently assigned to a user.
Uses a single `GET /users/{id}?include=roles` request and reads the
role resources from the JSON:API `included` section. This request
requires MANAGE_USERS (it reads the user record).
The included role resources do not carry their `users` /
`provider_groups` relationships, so the returned `DetailedRole`
instances omit `user_ids` / `provider_group_ids` (unknown here)
rather than reporting them as empty. Use `get_role` for a role's full
assignment and provider-group scope.
Args:
user_id: The Prowler UUID of the user
Returns:
The user's roles as DetailedRole instances (empty list if none)
"""
response = await self.api_client.get(
f"/users/{user_id}", params={"include": "roles"}
)
included = response.get("included", []) or []
return [
DetailedRole.from_api_response(item)
for item in included
if item.get("type") == "roles"
]
@@ -0,0 +1,121 @@
"""User management tools for Prowler MCP Server.
This module provides read-only tools for viewing the users that belong to the
authenticated tenant, including identifying which user the current credentials
(API key or JWT) authenticate as.
"""
from typing import Any
from pydantic import Field
from prowler_mcp_server.prowler_app.models.users import (
DetailedUser,
UsersListResponse,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
class UsersTools(BaseTool):
"""Tools for user management operations (read-only).
Provides tools for:
- prowler_list_users: List the users in the tenant with their names and emails
- prowler_get_user: Get detailed information about a specific user by ID
- prowler_get_current_user: Identify which user the current credentials authenticate as
"""
async def list_users(
self,
name: str | None = Field(
default=None,
description="Filter by user display name. Partial match supported (case-insensitive).",
),
email: str | None = Field(
default=None,
description="Filter by user email address. Partial match supported (case-insensitive).",
),
page_size: int = Field(
default=50, description="Number of results to return per page"
),
page_number: int = Field(
default=1, description="Page number to retrieve (1-indexed)"
),
) -> dict[str, Any]:
"""List the users that belong to the authenticated tenant.
Use this to see who has access to the tenant and to look up their email
addresses. Returns LIGHTWEIGHT user information optimized for browsing.
Each user includes:
- id: Prowler internal UUID (v4), used with `prowler_get_user`
- name: Display name
- email: Email address
- company_name: Company the user belongs to, when set
To find out which user the current credentials authenticate as, use
`prowler_get_current_user`. For a single user's roles, membership links
and join date, use `prowler_get_user`.
"""
self.api_client.validate_page_size(page_size)
params: dict[str, Any] = {
"fields[users]": "name,email,company_name",
"page[number]": page_number,
"page[size]": page_size,
}
if name:
params["filter[name__icontains]"] = name
if email:
params["filter[email__icontains]"] = email
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get("/users", params=clean_params)
simplified_response = UsersListResponse.from_api_response(api_response)
return simplified_response.model_dump()
async def get_user(
self,
user_id: str = Field(
description="Prowler's internal UUID (v4) for the user to retrieve. Use `prowler_list_users` to find user IDs if you only know a name or email."
),
) -> dict[str, Any]:
"""Retrieve detailed information about a specific user by their ID.
Returns everything `prowler_list_users` returns PLUS:
- date_joined: When the user joined
- role_ids: UUIDs of the roles assigned to the user
- membership_ids: UUIDs of the user's tenant memberships
Reading another user's roles/memberships requires MANAGE_ACCOUNT; without
it the API hides them and `role_ids`/`membership_ids` are omitted rather
than reported as empty.
Workflow:
1. Use `prowler_list_users` to browse users and find the target user 'id'
2. Use this tool with that 'id' to inspect the user's roles and account details
"""
api_response = await self.api_client.get(f"/users/{user_id}")
detailed_user = DetailedUser.from_api_response(api_response["data"])
return detailed_user.model_dump()
async def get_current_user(self) -> dict[str, Any]:
"""Identify which user the current credentials authenticate as.
Use this to determine the identity behind the credentials this MCP server
is currently using, e.g. before performing actions on behalf of that user
or when reporting who is connected.
Returns the same detailed information as `prowler_get_user`:
- id, name, email, company_name
- date_joined
- role_ids, membership_ids
"""
api_response = await self.api_client.get("/users/me")
detailed_user = DetailedUser.from_api_response(api_response["data"])
return detailed_user.model_dump()
@@ -176,13 +176,19 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
)
async def delete(
self, path: str, params: dict[str, any] | None = None
self,
path: str,
params: dict[str, any] | None = None,
json_data: dict[str, any] | None = None,
) -> dict[str, any]:
"""Make DELETE request.
Args:
path: API endpoint path
params: Optional query parameters
json_data: Optional JSON body data. Some JSON:API relationship
endpoints (e.g. ``/users/{id}/relationships/roles``) accept a
body listing the specific members to remove.
Returns:
API response as dictionary
@@ -190,7 +196,9 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
Raises:
Exception: If API request fails
"""
return await self._make_request(HTTPMethod.DELETE, path, params=params)
return await self._make_request(
HTTPMethod.DELETE, path, params=params, json_data=json_data
)
async def fetch_external_url(self, url: str) -> str:
"""Fetch content from an allowed external URL (unauthenticated).