Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.277"
VERSION = "0.261.278"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
50 changes: 44 additions & 6 deletions application/single_app/functions_activity_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,21 @@
from config import cosmos_activity_logs_container


def build_activity_log_id(activity_type, user_id, idempotency_key):
"""Build the stable activity record ID used for idempotent writes."""
if not isinstance(idempotency_key, str) or not idempotency_key or len(idempotency_key) > 512:
raise ValueError("The activity idempotency key is invalid.")
return str(uuid.uuid5(
uuid.NAMESPACE_URL,
f"simplechat:{activity_type}:{user_id}:{idempotency_key}",
))


def _create_activity_record(record, idempotency_key=None):
if idempotency_key is not None:
if not isinstance(idempotency_key, str) or not idempotency_key or len(idempotency_key) > 512:
raise ValueError("The activity idempotency key is invalid.")
record["id"] = str(uuid.uuid5(
uuid.NAMESPACE_URL,
f"simplechat:{record['activity_type']}:{record['user_id']}:{idempotency_key}",
))
record["id"] = build_activity_log_id(
record['activity_type'], record['user_id'], idempotency_key
)
try:
cosmos_activity_logs_container.create_item(body=record)
except CosmosResourceExistsError:
Expand All @@ -33,6 +40,37 @@
return record


def has_activity_log_for_resource(user_id, activity_type, resource_id, workspace_type=None):
"""Check for an existing creation record within its user partition."""
if not user_id or not resource_id:
return False

if activity_type == 'conversation_creation':

Check warning on line 48 in application/single_app/functions_activity_logging.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
resource_path = 'c.conversation.conversation_id'

Check warning on line 49 in application/single_app/functions_activity_logging.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
elif activity_type == 'document_creation':
resource_path = 'c.document.document_id'
else:
raise ValueError("Unsupported activity type for resource lookup.")

query = (
"SELECT TOP 1 VALUE c.id FROM c "
f"WHERE c.activity_type = @activity_type AND {resource_path} = @resource_id"
)
parameters = [
{'name': '@activity_type', 'value': activity_type},
{'name': '@resource_id', 'value': resource_id},
]
if workspace_type:
query += " AND c.workspace_type = @workspace_type"
parameters.append({'name': '@workspace_type', 'value': workspace_type})
matches = cosmos_activity_logs_container.query_items(

Check warning on line 66 in application/single_app/functions_activity_logging.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
query=query,
parameters=parameters,
partition_key=user_id,
)
return next(iter(matches), None) is not None


def coerce_activity_log_user_id(user_id: Any) -> str:
"""Extract a stable string user id from a scalar or session-style identity payload."""
if user_id is None:
Expand Down
99 changes: 63 additions & 36 deletions application/single_app/functions_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,47 @@
return "Forbidden", 403
return decorated_function

def get_control_center_capabilities(user=None, settings=None):
"""Return the Control Center permissions implied by the user's roles and settings."""

Check warning on line 963 in application/single_app/functions_authentication.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
user = session.get('user', {}) if user is None else user
settings = get_settings() if settings is None else settings

Check warning on line 965 in application/single_app/functions_authentication.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
require_admin_role = settings.get("require_member_of_control_center_admin", False)

Check warning on line 966 in application/single_app/functions_authentication.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
require_dashboard_reader_role = settings.get(

Check warning on line 967 in application/single_app/functions_authentication.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
"require_member_of_control_center_dashboard_reader", False
)

has_control_center_admin_role = (

Check warning on line 971 in application/single_app/functions_authentication.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
'roles' in user and 'ControlCenterAdmin' in user['roles']

Check warning on line 972 in application/single_app/functions_authentication.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
if isinstance(user, dict)
else False
)
has_dashboard_reader_role = (

Check warning on line 976 in application/single_app/functions_authentication.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
'roles' in user and 'ControlCenterDashboardReader' in user['roles']
if isinstance(user, dict)
else False
)
has_regular_admin_role = (
'roles' in user and 'Admin' in user['roles']
if isinstance(user, dict)
else False
)

has_full_access = (
has_control_center_admin_role if require_admin_role else has_regular_admin_role
)
can_view_dashboard = has_full_access or (
require_dashboard_reader_role and has_dashboard_reader_role
)
return {
'can_view_dashboard': bool(can_view_dashboard),
'can_manage_users': bool(has_full_access),
'can_manage_groups': bool(has_full_access),
'can_manage_workspaces': bool(has_full_access),
'can_view_activity_logs': bool(has_full_access),
'can_run_maintenance': bool(has_full_access),
}


def control_center_required(access_level='admin'):
"""
Unified Control Center access control decorator.
Expand All @@ -984,48 +1025,34 @@
user = session.get('user', {})
settings = get_settings()
require_member_of_control_center_admin = settings.get("require_member_of_control_center_admin", False)
require_member_of_control_center_dashboard_reader = settings.get("require_member_of_control_center_dashboard_reader", False)

has_control_center_admin_role = 'roles' in user and 'ControlCenterAdmin' in user['roles']
has_dashboard_reader_role = 'roles' in user and 'ControlCenterDashboardReader' in user['roles']
has_regular_admin_role = 'roles' in user and 'Admin' in user['roles']

# Check if ControlCenterAdmin role requirement is enforced
if require_member_of_control_center_admin:
# ControlCenterAdmin role is REQUIRED for access
# Only ControlCenterAdmin role grants full access
if has_control_center_admin_role:
return f(*args, **kwargs)

# For dashboard access, check if DashboardReader role grants access
if access_level == 'dashboard':
if require_member_of_control_center_dashboard_reader and has_dashboard_reader_role:
return f(*args, **kwargs)

# User doesn't have ControlCenterAdmin role, deny access
# Note: Regular Admin role does NOT grant access when this setting is enabled
is_api_request = (request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html) or request.path.startswith('/api/')
if is_api_request:
return jsonify({"error": "Forbidden", "message": "Insufficient permissions (ControlCenterAdmin role required)"}), 403
else:
return "Forbidden: ControlCenterAdmin role required", 403

# ControlCenterAdmin requirement is NOT enforced (default behavior)
# Only regular Admin role grants access - ControlCenterAdmin role is IGNORED
if has_regular_admin_role:
capabilities = get_control_center_capabilities(user, settings)
permitted = (
capabilities['can_view_dashboard']
if access_level == 'dashboard'
else capabilities['can_manage_users']
)
if permitted:
return f(*args, **kwargs)

# For dashboard-only access, check if DashboardReader role is enabled and user has it
if access_level == 'dashboard':
if require_member_of_control_center_dashboard_reader and has_dashboard_reader_role:
return f(*args, **kwargs)

# User is not an admin and doesn't have special roles - deny access
is_api_request = (request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html) or request.path.startswith('/api/')
if is_api_request:
return jsonify({"error": "Forbidden", "message": "Insufficient permissions (Admin role required)"}), 403
required_role = (
"ControlCenterAdmin"
if require_member_of_control_center_admin
else "Admin"
)
return jsonify({
"error": "Forbidden",
"message": f"Insufficient permissions ({required_role} role required)",
}), 403
else:
return "Forbidden: Admin role required", 403
required_role = (
"ControlCenterAdmin"
if require_member_of_control_center_admin
else "Admin"
)
return f"Forbidden: {required_role} role required", 403
return decorated_function
return decorator

Expand Down
70 changes: 65 additions & 5 deletions application/single_app/route_backend_control_center.py
Original file line number Diff line number Diff line change
Expand Up @@ -6208,13 +6208,18 @@ def api_migrate_to_activity_logs():
results = {
'conversations_migrated': 0,
'conversations_failed': 0,
'conversations_skipped_existing': 0,
'personal_documents_migrated': 0,
'personal_documents_failed': 0,
'personal_documents_skipped_existing': 0,
'group_documents_migrated': 0,
'group_documents_failed': 0,
'group_documents_skipped_existing': 0,
'public_documents_migrated': 0,
'public_documents_failed': 0,
'public_documents_skipped_existing': 0,
'total_migrated': 0,
'total_skipped_existing': 0,
'total_failed': 0,
'errors': []
}
Expand All @@ -6236,9 +6241,21 @@ def api_migrate_to_activity_logs():

for conv in conversations:
try:
if has_activity_log_for_resource(
conv.get('user_id'), 'conversation_creation', conv.get('id')
):
conv['added_to_activity_log'] = True
cosmos_conversations_container.upsert_item(conv)
results['conversations_skipped_existing'] += 1
continue

# Create activity log directly to preserve original timestamp
activity_log = {
'id': str(uuid.uuid4()),
'id': build_activity_log_id(
'conversation_creation',
conv.get('user_id'),
f"backfill:{conv.get('id')}",
),
'activity_type': 'conversation_creation',
'user_id': conv.get('user_id'),
'timestamp': conv.get('created_at') or conv.get('last_updated') or datetime.utcnow().isoformat(),
Expand Down Expand Up @@ -6288,9 +6305,21 @@ def api_migrate_to_activity_logs():

for doc in personal_docs:
try:
if has_activity_log_for_resource(
doc.get('user_id'), 'document_creation', doc.get('id'), 'personal'
):
doc['added_to_activity_log'] = True
cosmos_user_documents_container.upsert_item(doc)
results['personal_documents_skipped_existing'] += 1
continue

# Create activity log directly to preserve original timestamp
activity_log = {
'id': str(uuid.uuid4()),
'id': build_activity_log_id(
'document_creation',
doc.get('user_id'),
f"backfill:personal:{doc.get('id')}",
),
'user_id': doc.get('user_id'),
'activity_type': 'document_creation',
'workspace_type': 'personal',
Expand Down Expand Up @@ -6354,9 +6383,21 @@ def api_migrate_to_activity_logs():

for doc in group_docs:
try:
if has_activity_log_for_resource(
doc.get('user_id'), 'document_creation', doc.get('id'), 'group'
):
doc['added_to_activity_log'] = True
cosmos_group_documents_container.upsert_item(doc)
results['group_documents_skipped_existing'] += 1
continue

# Create activity log directly to preserve original timestamp
activity_log = {
'id': str(uuid.uuid4()),
'id': build_activity_log_id(
'document_creation',
doc.get('user_id'),
f"backfill:group:{doc.get('id')}",
),
'user_id': doc.get('user_id'),
'activity_type': 'document_creation',
'workspace_type': 'group',
Expand Down Expand Up @@ -6422,9 +6463,21 @@ def api_migrate_to_activity_logs():

for doc in public_docs:
try:
if has_activity_log_for_resource(
doc.get('user_id'), 'document_creation', doc.get('id'), 'public'
):
doc['added_to_activity_log'] = True
cosmos_public_documents_container.upsert_item(doc)
results['public_documents_skipped_existing'] += 1
continue

# Create activity log directly to preserve original timestamp
activity_log = {
'id': str(uuid.uuid4()),
'id': build_activity_log_id(
'document_creation',
doc.get('user_id'),
f"backfill:public:{doc.get('id')}",
),
'user_id': doc.get('user_id'),
'activity_type': 'document_creation',
'workspace_type': 'public',
Expand Down Expand Up @@ -6482,7 +6535,14 @@ def api_migrate_to_activity_logs():
results['group_documents_migrated'] +
results['public_documents_migrated']
)


results['total_skipped_existing'] = (
results['conversations_skipped_existing'] +
results['personal_documents_skipped_existing'] +
results['group_documents_skipped_existing'] +
results['public_documents_skipped_existing']
)

results['total_failed'] = (
results['conversations_failed'] +
results['personal_documents_failed'] +
Expand Down
2 changes: 2 additions & 0 deletions application/single_app/route_backend_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,8 @@ def user_settings():
# Whether the V2 Admin Settings categories rail is collapsed to icons.
# Separate from the shell and workspace rails so each keeps its own state.
'v2AdminRailCollapsed',
# Whether the V2 Control Center section rail is collapsed to icons.
'v2ControlCenterRailCollapsed',
# V2 document explorer: how the workspace documents list is presented
# (view mode, visible columns, page size, details pane) and the saved
# filter combinations pinned in its navigation rail.
Expand Down
2 changes: 2 additions & 0 deletions application/single_app/route_backend_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
)
from functions_authentication import (
admin_required,
get_control_center_capabilities,
get_current_user_id,
get_current_user_info,
login_required,
Expand Down Expand Up @@ -872,6 +873,7 @@ def v2_bootstrap():
"navigation": _build_navigation(settings, current_user_roles),
"features": _build_feature_flags(public_settings, per_user_overrides),
"capabilities": _build_capabilities(settings),
"control_center": get_control_center_capabilities(session_user, settings),
"orchestration": _build_orchestration(settings, public_settings),
"catalogs": {
"models": models,
Expand Down
Loading
Loading