Sensitive Fields¶
Sensitive fields provide field-level access control via model annotations. Mark individual fields as sensitive, and the framework automatically strips them from responses or blocks writes based on the caller's capabilities.
SensitiveField annotation¶
Use SensitiveField() instead of Field() to annotate fields that require capability checks:
from craft_easy.core.access.sensitive import SensitiveField
from craft_easy.core.crud.base import BaseDocument
from decimal import Decimal
from typing import Optional
class Payment(BaseDocument):
reference: str
amount: Decimal = SensitiveField(..., read="payments:read_amount")
bank_account: Optional[str] = SensitiveField(
None,
read="payments:read_bank",
write="payments:write_bank",
)
status: str
Parameters¶
| Parameter | Type | Description |
|---|---|---|
default |
any | Default value (same as Pydantic Field()) |
read |
str (optional) |
Capability required to see this field in responses |
write |
str (optional) |
Capability required to include this field in create/update |
**kwargs |
any | All other Pydantic Field() parameters |
If only read is set, anyone can write the field but only users with the capability can read it. If only write is set, anyone can read but only users with the capability can write. Set both for full control.
Read filtering¶
When a response is returned, filter_sensitive() strips fields the caller cannot read:
from craft_easy.core.access.sensitive_filter import filter_sensitive
# User WITHOUT "payments:read_amount" capability
filtered = filter_sensitive(Payment, payment_data, access)
# Result: {"reference": "PAY-001", "status": "completed"}
# "amount" and "bank_account" are stripped
# User WITH "payments:read_amount" but not "payments:read_bank"
filtered = filter_sensitive(Payment, payment_data, access)
# Result: {"reference": "PAY-001", "amount": "150.00", "status": "completed"}
# "bank_account" is stripped
For list endpoints, filter_sensitive_list() applies the same filtering to every item:
from craft_easy.core.access.sensitive_filter import filter_sensitive_list
filtered_items = filter_sensitive_list(Payment, payments, access)
Note
For standard CRUD resources, sensitive field filtering is automatically applied by Resource.build(). You only need to call these functions manually for custom endpoints.
Write checking¶
When a create or update request includes a sensitive field, check_sensitive_write() verifies the caller has the required write capability:
from craft_easy.core.access.sensitive_filter import check_sensitive_write
# Raises SensitiveFieldWriteDenied if caller lacks "payments:write_bank"
check_sensitive_write(Payment, request_data, access)
If the caller tries to write a field they lack the capability for, the API returns a 403 error:
{
"detail": {
"error": "sensitive_field_write_denied",
"field": "bank_account",
"required": "payments:write_bank"
}
}
Wildcard support¶
Both annotations and user capabilities support wildcards with *:
# Annotation: any "payments:*" capability satisfies this
amount: Decimal = SensitiveField(..., read="payments:*")
# Or: user holds "payments:*" which matches any "payments:..." requirement
The has_capability() function handles wildcard matching in both directions:
- Annotation wildcard:
read="payments:*"matches if the user holds any capability starting withpayments: - User wildcard: User holds
payments:*and field requirespayments:read_amount→ match
Auto-registration¶
When a resource is registered with Resource.build(), all capabilities referenced by SensitiveField annotations are automatically registered in the CapabilityRegistry:
from craft_easy.core.access.sensitive import register_sensitive_capabilities
# Called automatically by Resource.build()
register_sensitive_capabilities(Payment)
# Registers: "payments:read_amount", "payments:read_bank", "payments:write_bank"
This is idempotent — registering the same capabilities multiple times is safe.
Introspection¶
You can inspect which fields on a model are sensitive:
from craft_easy.core.access.sensitive import get_sensitive_fields
fields = get_sensitive_fields(Payment)
# Returns:
# {
# "amount": {"read": "payments:read_amount"},
# "bank_account": {"read": "payments:read_bank", "write": "payments:write_bank"},
# }
Integration with CRUD¶
The standard CRUD operations in Resource integrate sensitive fields automatically:
| Operation | Sensitive field behavior |
|---|---|
| List (GET /) | filter_sensitive_list() strips unreadable fields from every item |
| Read (GET /{id}) | filter_sensitive() strips unreadable fields |
| Create (POST /) | check_sensitive_write() blocks writes to protected fields |
| Update (PATCH /{id}) | check_sensitive_write() blocks writes to protected fields |
Testing sensitive fields¶
Verify that fields are correctly filtered by making requests with different capability sets:
# User with payments:read_amount — sees amount
curl http://localhost:5001/payments/664f1a2b3c4d5e6f7a8b9c0d \
-H "Authorization: Bearer $OPERATOR_TOKEN"
# Response includes "amount": "150.00"
# User without payments:read_amount — amount stripped
curl http://localhost:5001/payments/664f1a2b3c4d5e6f7a8b9c0d \
-H "Authorization: Bearer $VIEWER_TOKEN"
# Response does NOT include "amount"
# User without payments:write_bank — write blocked
curl -X PATCH http://localhost:5001/payments/664f1a2b3c4d5e6f7a8b9c0d \
-H "Authorization: Bearer $VIEWER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"bank_account": "SE1234567890"}'
# 403: {"detail": {"error": "sensitive_field_write_denied", "field": "bank_account", ...}}