Phase 1: Critical security fixes - 59 permissions, grants, RLS, mass-assignment, ownership, leaks, MIME
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
"""Restrict DELETE grants on sensitive tables.
|
||||||
|
|
||||||
|
Removes DELETE privilege from crm_api and crm_worker on:
|
||||||
|
api_tokens, audit_log, notification_types, password_reset_tokens,
|
||||||
|
plugin_allowlist, plugin_migrations, plugins, sessions,
|
||||||
|
tenant_plugin_activation, tenants, user_tenants, users.
|
||||||
|
|
||||||
|
crm_auth keeps DELETE on sessions + password_reset_tokens (for logout/reset).
|
||||||
|
|
||||||
|
Revision ID: 0100
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "0100"
|
||||||
|
down_revision = "0099"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
# Tables where DELETE must be removed from crm_api and crm_worker
|
||||||
|
SENSITIVE_TABLES = [
|
||||||
|
"api_tokens",
|
||||||
|
"audit_log",
|
||||||
|
"notification_types",
|
||||||
|
"password_reset_tokens",
|
||||||
|
"plugin_allowlist",
|
||||||
|
"plugin_migrations",
|
||||||
|
"plugins",
|
||||||
|
"sessions",
|
||||||
|
"tenant_plugin_activation",
|
||||||
|
"tenants",
|
||||||
|
"user_tenants",
|
||||||
|
"users",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
for table in SENSITIVE_TABLES:
|
||||||
|
op.execute(f"REVOKE DELETE ON TABLE {table} FROM crm_api;")
|
||||||
|
op.execute(f"REVOKE DELETE ON TABLE {table} FROM crm_worker;")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
for table in SENSITIVE_TABLES:
|
||||||
|
op.execute(f"GRANT DELETE ON TABLE {table} TO crm_api;")
|
||||||
|
op.execute(f"GRANT DELETE ON TABLE {table} TO crm_worker;")
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""Enable RLS on critical tables missing it.
|
||||||
|
|
||||||
|
Tables: api_tokens, sequences, sessions, tenant_plugin_activation,
|
||||||
|
user_tenants, password_reset_tokens.
|
||||||
|
Also adds tenant_id to guest_invitations and enables RLS.
|
||||||
|
|
||||||
|
Revision ID: 0101
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = "0101"
|
||||||
|
down_revision = "0100"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
# Tables that have tenant_id but no RLS
|
||||||
|
RLS_TABLES = [
|
||||||
|
"api_tokens",
|
||||||
|
"sequences",
|
||||||
|
"sessions",
|
||||||
|
"tenant_plugin_activation",
|
||||||
|
"user_tenants",
|
||||||
|
"password_reset_tokens",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Enable RLS + create tenant isolation policy for each table
|
||||||
|
for table in RLS_TABLES:
|
||||||
|
op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY;")
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {table}_tenant_isolation ON {table} "
|
||||||
|
f"FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid) "
|
||||||
|
f"WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);"
|
||||||
|
)
|
||||||
|
|
||||||
|
# guest_invitations: add tenant_id + enable RLS
|
||||||
|
op.add_column("guest_invitations", sa.Column("tenant_id", sa.UUID(), nullable=True))
|
||||||
|
op.execute("CREATE INDEX ix_guest_invitations_tenant_id ON guest_invitations (tenant_id);")
|
||||||
|
op.execute("ALTER TABLE guest_invitations ENABLE ROW LEVEL SECURITY;")
|
||||||
|
op.execute(
|
||||||
|
"CREATE POLICY guest_invitations_tenant_isolation ON guest_invitations "
|
||||||
|
"FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid) "
|
||||||
|
"WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Drop guest_invitations RLS + tenant_id
|
||||||
|
op.execute("DROP POLICY IF EXISTS guest_invitations_tenant_isolation ON guest_invitations;")
|
||||||
|
op.execute("ALTER TABLE guest_invitations DISABLE ROW LEVEL SECURITY;")
|
||||||
|
op.execute("DROP INDEX IF EXISTS ix_guest_invitations_tenant_id;")
|
||||||
|
op.drop_column("guest_invitations", "tenant_id")
|
||||||
|
|
||||||
|
# Drop RLS on other tables
|
||||||
|
for table in RLS_TABLES:
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {table}_tenant_isolation ON {table};")
|
||||||
|
op.execute(f"ALTER TABLE {table} DISABLE ROW LEVEL SECURITY;")
|
||||||
@@ -66,6 +66,66 @@ CORE_PERMISSIONS: list[dict[str, str]] = [
|
|||||||
{"key": "workspaces:assign_users", "label": "Workspaces: Assign Users", "category": "core", "module": "workspaces"},
|
{"key": "workspaces:assign_users", "label": "Workspaces: Assign Users", "category": "core", "module": "workspaces"},
|
||||||
{"key": "workspaces:configure_modules", "label": "Workspaces: Configure Modules", "category": "core", "module": "workspaces"},
|
{"key": "workspaces:configure_modules", "label": "Workspaces: Configure Modules", "category": "core", "module": "workspaces"},
|
||||||
{"key": "system:admin", "label": "System: Admin (cross-tenant)", "category": "system", "module": "system"},
|
{"key": "system:admin", "label": "System: Admin (cross-tenant)", "category": "system", "module": "system"},
|
||||||
|
# ── Plugin permissions (registered at startup, but also listed here for completeness) ──
|
||||||
|
{"key": "ai:read", "label": "AI: Read", "category": "core", "module": "ai"},
|
||||||
|
{"key": "ai:write", "label": "AI: Write", "category": "core", "module": "ai"},
|
||||||
|
{"key": "ai:agents", "label": "AI: Agents", "category": "core", "module": "ai"},
|
||||||
|
{"key": "ai:config", "label": "AI: Config", "category": "core", "module": "ai"},
|
||||||
|
{"key": "ai_proactive:read", "label": "AI Proactive: Read", "category": "core", "module": "ai_proactive"},
|
||||||
|
{"key": "ai_proactive:write", "label": "AI Proactive: Write", "category": "core", "module": "ai_proactive"},
|
||||||
|
{"key": "ai_proactive:config", "label": "AI Proactive: Config", "category": "core", "module": "ai_proactive"},
|
||||||
|
{"key": "agents:read", "label": "Agents: Read", "category": "core", "module": "agents"},
|
||||||
|
{"key": "agents:write", "label": "Agents: Write", "category": "core", "module": "agents"},
|
||||||
|
{"key": "agents:delete", "label": "Agents: Delete", "category": "core", "module": "agents"},
|
||||||
|
{"key": "agents:execute", "label": "Agents: Execute", "category": "core", "module": "agents"},
|
||||||
|
{"key": "automation:read", "label": "Automation: Read", "category": "core", "module": "automation"},
|
||||||
|
{"key": "automation:write", "label": "Automation: Write", "category": "core", "module": "automation"},
|
||||||
|
{"key": "automation:delete", "label": "Automation: Delete", "category": "core", "module": "automation"},
|
||||||
|
{"key": "automation:execute", "label": "Automation: Execute", "category": "core", "module": "automation"},
|
||||||
|
{"key": "automation:admin", "label": "Automation: Admin", "category": "core", "module": "automation"},
|
||||||
|
{"key": "automation:configure", "label": "Automation: Configure", "category": "core", "module": "automation"},
|
||||||
|
{"key": "calendar:read", "label": "Calendar: Read", "category": "core", "module": "calendar"},
|
||||||
|
{"key": "calendar:write", "label": "Calendar: Write", "category": "core", "module": "calendar"},
|
||||||
|
{"key": "calendar:delete", "label": "Calendar: Delete", "category": "core", "module": "calendar"},
|
||||||
|
{"key": "calendar:share", "label": "Calendar: Share", "category": "core", "module": "calendar"},
|
||||||
|
{"key": "comm:read", "label": "Comm: Read", "category": "core", "module": "comm"},
|
||||||
|
{"key": "comm:write", "label": "Comm: Write", "category": "core", "module": "comm"},
|
||||||
|
{"key": "comm:delete", "label": "Comm: Delete", "category": "core", "module": "comm"},
|
||||||
|
{"key": "comm:manage", "label": "Comm: Manage", "category": "core", "module": "comm"},
|
||||||
|
{"key": "dashboard:read", "label": "Dashboard: Read", "category": "core", "module": "dashboard"},
|
||||||
|
{"key": "dms:read", "label": "DMS: Read", "category": "core", "module": "dms"},
|
||||||
|
{"key": "dms:write", "label": "DMS: Write", "category": "core", "module": "dms"},
|
||||||
|
{"key": "dms:delete", "label": "DMS: Delete", "category": "core", "module": "dms"},
|
||||||
|
{"key": "dms:share", "label": "DMS: Share", "category": "core", "module": "dms"},
|
||||||
|
{"key": "entity_links:read", "label": "Entity Links: Read", "category": "core", "module": "entity_links"},
|
||||||
|
{"key": "entity_links:write", "label": "Entity Links: Write", "category": "core", "module": "entity_links"},
|
||||||
|
{"key": "entity_links:delete", "label": "Entity Links: Delete", "category": "core", "module": "entity_links"},
|
||||||
|
{"key": "mail:read", "label": "Mail: Read", "category": "core", "module": "mail"},
|
||||||
|
{"key": "mail:write", "label": "Mail: Write", "category": "core", "module": "mail"},
|
||||||
|
{"key": "mail:delete", "label": "Mail: Delete", "category": "core", "module": "mail"},
|
||||||
|
{"key": "mail:send", "label": "Mail: Send", "category": "core", "module": "mail"},
|
||||||
|
{"key": "mail:share", "label": "Mail: Share", "category": "core", "module": "mail"},
|
||||||
|
{"key": "mail:config", "label": "Mail: Config", "category": "core", "module": "mail"},
|
||||||
|
{"key": "mcp:read", "label": "MCP: Read", "category": "core", "module": "mcp"},
|
||||||
|
{"key": "mcp:write", "label": "MCP: Write", "category": "core", "module": "mcp"},
|
||||||
|
{"key": "permissions:admin", "label": "Permissions: Admin", "category": "core", "module": "permissions"},
|
||||||
|
{"key": "permissions:delegations:read", "label": "Permissions: Delegations: Read", "category": "core", "module": "permissions"},
|
||||||
|
{"key": "permissions:delegations:write", "label": "Permissions: Delegations: Write", "category": "core", "module": "permissions"},
|
||||||
|
{"key": "permissions:policies:read", "label": "Permissions: Policies: Read", "category": "core", "module": "permissions"},
|
||||||
|
{"key": "permissions:policies:write", "label": "Permissions: Policies: Write", "category": "core", "module": "permissions"},
|
||||||
|
{"key": "permissions:templates:read", "label": "Permissions: Templates: Read", "category": "core", "module": "permissions"},
|
||||||
|
{"key": "permissions:templates:write", "label": "Permissions: Templates: Write", "category": "core", "module": "permissions"},
|
||||||
|
{"key": "reports:read", "label": "Reports: Read", "category": "core", "module": "reports"},
|
||||||
|
{"key": "reports:generate", "label": "Reports: Generate", "category": "core", "module": "reports"},
|
||||||
|
{"key": "reports:manage_templates", "label": "Reports: Manage Templates", "category": "core", "module": "reports"},
|
||||||
|
{"key": "search:read", "label": "Search: Read", "category": "core", "module": "search"},
|
||||||
|
{"key": "search:admin", "label": "Search: Admin", "category": "core", "module": "search"},
|
||||||
|
{"key": "tags:read", "label": "Tags: Read", "category": "core", "module": "tags"},
|
||||||
|
{"key": "tags:write", "label": "Tags: Write", "category": "core", "module": "tags"},
|
||||||
|
{"key": "tags:delete", "label": "Tags: Delete", "category": "core", "module": "tags"},
|
||||||
|
{"key": "tasks:read", "label": "Tasks: Read", "category": "core", "module": "tasks"},
|
||||||
|
{"key": "tasks:write", "label": "Tasks: Write", "category": "core", "module": "tasks"},
|
||||||
|
{"key": "tasks:delete", "label": "Tasks: Delete", "category": "core", "module": "tasks"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,40 @@ def _sanitize_filename(filename: str) -> str:
|
|||||||
BLOCKED_EXTENSIONS = {
|
BLOCKED_EXTENSIONS = {
|
||||||
".exe", ".bat", ".cmd", ".sh", ".jar", ".com", ".scr", ".msi",
|
".exe", ".bat", ".cmd", ".sh", ".jar", ".com", ".scr", ".msi",
|
||||||
".dll", ".vbs", ".ps1", ".app", ".bin", ".reg", ".inf",
|
".dll", ".vbs", ".ps1", ".app", ".bin", ".reg", ".inf",
|
||||||
|
".php", ".py", ".pl", ".asp", ".aspx", ".jsp", ".svg", ".htaccess",
|
||||||
|
".phtml", ".pht", ".cgi", ".cfm", ".erb",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Allowed MIME types for upload validation
|
||||||
|
ALLOWED_MIME_PREFIXES = {
|
||||||
|
"application/pdf",
|
||||||
|
"application/msword",
|
||||||
|
"application/vnd.openxmlformats-officedocument",
|
||||||
|
"application/vnd.oasis.opendocument",
|
||||||
|
"application/vnd.ms-excel",
|
||||||
|
"application/vnd.ms-powerpoint",
|
||||||
|
"application/zip",
|
||||||
|
"application/gzip",
|
||||||
|
"application/x-tar",
|
||||||
|
"application/json",
|
||||||
|
"application/xml",
|
||||||
|
"application/rtf",
|
||||||
|
"application/x-7z-compressed",
|
||||||
|
"application/x-rar-compressed",
|
||||||
|
"text/plain",
|
||||||
|
"text/csv",
|
||||||
|
"text/html",
|
||||||
|
"text/markdown",
|
||||||
|
"image/png",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/gif",
|
||||||
|
"image/webp",
|
||||||
|
"image/bmp",
|
||||||
|
"image/tiff",
|
||||||
|
"image/x-icon",
|
||||||
|
"audio/",
|
||||||
|
"video/",
|
||||||
|
"application/octet-stream",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -467,6 +501,14 @@ async def upload_file(
|
|||||||
detail={"detail": "File type not allowed", "code": "blocked_filetype"},
|
detail={"detail": "File type not allowed", "code": "blocked_filetype"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# MIME type validation: verify content_type against allowlist
|
||||||
|
mime_type = file.content_type or "application/octet-stream"
|
||||||
|
if not any(mime_type.startswith(prefix) for prefix in ALLOWED_MIME_PREFIXES):
|
||||||
|
raise HTTPException(
|
||||||
|
400,
|
||||||
|
detail={"detail": f"MIME type '{mime_type}' not allowed", "code": "blocked_mimetype"},
|
||||||
|
)
|
||||||
|
|
||||||
# Validate folder exists if specified
|
# Validate folder exists if specified
|
||||||
if fid is not None:
|
if fid is not None:
|
||||||
folder_result = await db.execute(
|
folder_result = await db.execute(
|
||||||
|
|||||||
@@ -86,6 +86,60 @@ async def create_entity_permission(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_entity_ownership(
|
||||||
|
db: AsyncSession,
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
|
user_id: uuid.UUID,
|
||||||
|
entity_type: str,
|
||||||
|
entity_id: str,
|
||||||
|
is_system_admin: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Verify that the current user owns the entity or is system admin.
|
||||||
|
|
||||||
|
Raises 403 if the user is neither owner nor system admin.
|
||||||
|
"""
|
||||||
|
if is_system_admin:
|
||||||
|
return
|
||||||
|
|
||||||
|
from app.models.entity_permission import ENTITY_MODELS
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
model_info = ENTITY_MODELS.get(entity_type)
|
||||||
|
if model_info is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail={"detail": f"Unknown entity type: {entity_type}", "code": "not_found"},
|
||||||
|
)
|
||||||
|
|
||||||
|
model = model_info["model"]
|
||||||
|
try:
|
||||||
|
eid = uuid.UUID(entity_id)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail={"detail": "Invalid entity_id", "code": "invalid_id"},
|
||||||
|
) from None
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(model.owner_id).where(
|
||||||
|
model.id == eid,
|
||||||
|
model.tenant_id == tenant_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
row = result.first()
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail={"detail": "Entity not found", "code": "not_found"},
|
||||||
|
)
|
||||||
|
owner_id = row[0]
|
||||||
|
if owner_id is not None and owner_id != user_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail={"detail": "Only the entity owner or system admin can modify permissions", "code": "forbidden"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{entity_type}/{entity_id}/{permission_id}")
|
@router.put("/{entity_type}/{entity_id}/{permission_id}")
|
||||||
async def update_entity_permission(
|
async def update_entity_permission(
|
||||||
entity_type: str,
|
entity_type: str,
|
||||||
@@ -103,6 +157,11 @@ async def update_entity_permission(
|
|||||||
_PERM_RATE_LIMIT_MAX,
|
_PERM_RATE_LIMIT_MAX,
|
||||||
_PERM_RATE_LIMIT_WINDOW,
|
_PERM_RATE_LIMIT_WINDOW,
|
||||||
)
|
)
|
||||||
|
# Ownership check: only owner or system admin can update permissions
|
||||||
|
await _check_entity_ownership(
|
||||||
|
db, tenant_id, user_id, entity_type, entity_id,
|
||||||
|
current_user.get("is_system_admin", False),
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
return await entity_permission_service.update_permission(
|
return await entity_permission_service.update_permission(
|
||||||
db,
|
db,
|
||||||
|
|||||||
@@ -20,11 +20,24 @@ async def get_system_settings(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: dict = Depends(require_permission("settings:read")),
|
current_user: dict = Depends(require_permission("settings:read")),
|
||||||
):
|
):
|
||||||
"""Get system settings for the current tenant."""
|
"""Get system settings for the current tenant.
|
||||||
|
|
||||||
|
Sensitive fields (tax_number, iban, bic) are masked for non-admin users.
|
||||||
|
"""
|
||||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
result = await system_settings_service.get_system_settings(db, tenant_id)
|
result = await system_settings_service.get_system_settings(db, tenant_id)
|
||||||
if result is None:
|
if result is None:
|
||||||
return SystemSettingsResponse()
|
return SystemSettingsResponse()
|
||||||
|
|
||||||
|
# Mask sensitive fields for non-admin users
|
||||||
|
if not current_user.get("is_system_admin"):
|
||||||
|
if hasattr(result, "tax_number") and result.tax_number:
|
||||||
|
result.tax_number = "********"
|
||||||
|
if hasattr(result, "iban") and result.iban:
|
||||||
|
result.iban = "********"
|
||||||
|
if hasattr(result, "bic") and result.bic:
|
||||||
|
result.bic = "********"
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+16
-1
@@ -63,6 +63,14 @@ async def create_user(
|
|||||||
user_id = uuid.UUID(current_user["user_id"])
|
user_id = uuid.UUID(current_user["user_id"])
|
||||||
role_id = _parse_role_id(body.role_id)
|
role_id = _parse_role_id(body.role_id)
|
||||||
|
|
||||||
|
# Mass-Assignment protection: only system admin can create admin users
|
||||||
|
role = body.role
|
||||||
|
if role == "admin" and not current_user.get("is_system_admin"):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail={"detail": "Only system admin can create admin users", "code": "role_escalation_forbidden"},
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = await user_service.create_user(
|
user = await user_service.create_user(
|
||||||
db,
|
db,
|
||||||
@@ -70,7 +78,7 @@ async def create_user(
|
|||||||
body.email,
|
body.email,
|
||||||
body.name,
|
body.name,
|
||||||
body.password,
|
body.password,
|
||||||
body.role,
|
role,
|
||||||
role_id,
|
role_id,
|
||||||
body.is_active,
|
body.is_active,
|
||||||
)
|
)
|
||||||
@@ -190,6 +198,13 @@ async def update_user(
|
|||||||
detail={"detail": "Cannot modify your own role or active status", "code": "self_modification_forbidden"},
|
detail={"detail": "Cannot modify your own role or active status", "code": "self_modification_forbidden"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Mass-Assignment protection: only system admin can change roles to admin
|
||||||
|
if body.role == "admin" and not current_user.get("is_system_admin"):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail={"detail": "Only system admin can assign admin role", "code": "role_escalation_forbidden"},
|
||||||
|
)
|
||||||
|
|
||||||
# Determine if role_id was explicitly sent (Pydantic v2)
|
# Determine if role_id was explicitly sent (Pydantic v2)
|
||||||
role_id_sent = "role_id" in body.model_fields_set
|
role_id_sent = "role_id" in body.model_fields_set
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ class AttachmentResponse(BaseModel):
|
|||||||
entity_type: str
|
entity_type: str
|
||||||
entity_id: str
|
entity_id: str
|
||||||
filename: str
|
filename: str
|
||||||
file_path: str
|
|
||||||
mime_type: str
|
mime_type: str
|
||||||
file_size: int
|
file_size: int
|
||||||
uploaded_by: str | None = None
|
uploaded_by: str | None = None
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ class SystemSettingsResponse(BaseModel):
|
|||||||
company_legal_form: str | None = None
|
company_legal_form: str | None = None
|
||||||
company_street: str = ""
|
company_street: str = ""
|
||||||
company_city: str = ""
|
company_city: str = ""
|
||||||
|
# Sensitive fields — masked in API response, only visible to system admin
|
||||||
|
tax_number: str | None = None
|
||||||
|
vat_id: str | None = None
|
||||||
|
iban: str | None = None
|
||||||
|
bic: str | None = None
|
||||||
company_zip: str = ""
|
company_zip: str = ""
|
||||||
company_country: str = ""
|
company_country: str = ""
|
||||||
tax_number: str | None = None
|
tax_number: str | None = None
|
||||||
|
|||||||
Reference in New Issue
Block a user