Skip to content

Commit 42fc43f

Browse files
paullizerCopilot
andcommitted
Merge latest V2 UI base
Resolve configuration and release note conflicts while retaining both branches' changes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2 parents eeb106e + b3c0b7b commit 42fc43f

59 files changed

Lines changed: 6809 additions & 506 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎application/single_app/admin_settings_fields.py‎

Lines changed: 291 additions & 31 deletions
Large diffs are not rendered by default.

‎application/single_app/admin_settings_nav.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,38 @@ def iter_tabs():
638638
yield group, tab
639639

640640

641+
# Tabs that existed before the information architecture rework, mapped to where
642+
# their content now lives. ``LEGACY_TAB_REDIRECTS`` in
643+
# ``static/js/admin/admin_sidebar_nav.js`` resolves the same ids for the
644+
# server-rendered page; this copy lets the server resolve a Latest Features
645+
# shortcut for the V2 surface, and a functional test keeps the two identical.
646+
LEGACY_TAB_REDIRECTS = {
647+
"governance": "feature-governance",
648+
"scale": "redis-caching",
649+
"general": "branding",
650+
"safety": "access-roles",
651+
"security": "secrets",
652+
"workspaces": "workspace-types",
653+
"search-extract": "web-research",
654+
"ai-models": "model-endpoints",
655+
"data-management": "backup",
656+
}
657+
658+
659+
def resolve_admin_tab_id(tab_id):
660+
"""Return the live tab id for a current or pre-rework tab id.
661+
662+
A leading ``#`` is accepted because Latest Features shortcuts store their
663+
target as a fragment. Returns None when the id names no tab, directly or
664+
through ``LEGACY_TAB_REDIRECTS``.
665+
"""
666+
candidate = str(tab_id or "").strip().lstrip("#")
667+
if not candidate:
668+
return None
669+
candidate = LEGACY_TAB_REDIRECTS.get(candidate, candidate)
670+
return candidate if candidate in get_tab_ids() else None
671+
672+
641673
def get_tab_ids():
642674
"""Return every tab id in navigation order."""
643675
return [tab["id"] for _, tab in iter_tabs()]

‎application/single_app/config.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
EXECUTOR_TYPE = 'thread'
102102
EXECUTOR_MAX_WORKERS = 30
103103
SESSION_TYPE = 'filesystem'
104-
VERSION = "0.261.276"
104+
VERSION = "0.261.278"
105105
IS_DEVELOPMENT = is_development_env_enabled()
106106

107107
# Opt-out for deployments where App Service Easy Auth is active but the platform

‎application/single_app/functions_activity_logging.py‎

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,21 @@
1616
from config import cosmos_activity_logs_container
1717

1818

19+
def build_activity_log_id(activity_type, user_id, idempotency_key):
20+
"""Build the stable activity record ID used for idempotent writes."""
21+
if not isinstance(idempotency_key, str) or not idempotency_key or len(idempotency_key) > 512:
22+
raise ValueError("The activity idempotency key is invalid.")
23+
return str(uuid.uuid5(
24+
uuid.NAMESPACE_URL,
25+
f"simplechat:{activity_type}:{user_id}:{idempotency_key}",
26+
))
27+
28+
1929
def _create_activity_record(record, idempotency_key=None):
2030
if idempotency_key is not None:
21-
if not isinstance(idempotency_key, str) or not idempotency_key or len(idempotency_key) > 512:
22-
raise ValueError("The activity idempotency key is invalid.")
23-
record["id"] = str(uuid.uuid5(
24-
uuid.NAMESPACE_URL,
25-
f"simplechat:{record['activity_type']}:{record['user_id']}:{idempotency_key}",
26-
))
31+
record["id"] = build_activity_log_id(
32+
record['activity_type'], record['user_id'], idempotency_key
33+
)
2734
try:
2835
cosmos_activity_logs_container.create_item(body=record)
2936
except CosmosResourceExistsError:
@@ -33,6 +40,37 @@ def _create_activity_record(record, idempotency_key=None):
3340
return record
3441

3542

43+
def has_activity_log_for_resource(user_id, activity_type, resource_id, workspace_type=None):
44+
"""Check for an existing creation record within its user partition."""
45+
if not user_id or not resource_id:
46+
return False
47+
48+
if activity_type == 'conversation_creation':
49+
resource_path = 'c.conversation.conversation_id'
50+
elif activity_type == 'document_creation':
51+
resource_path = 'c.document.document_id'
52+
else:
53+
raise ValueError("Unsupported activity type for resource lookup.")
54+
55+
query = (
56+
"SELECT TOP 1 VALUE c.id FROM c "
57+
f"WHERE c.activity_type = @activity_type AND {resource_path} = @resource_id"
58+
)
59+
parameters = [
60+
{'name': '@activity_type', 'value': activity_type},
61+
{'name': '@resource_id', 'value': resource_id},
62+
]
63+
if workspace_type:
64+
query += " AND c.workspace_type = @workspace_type"
65+
parameters.append({'name': '@workspace_type', 'value': workspace_type})
66+
matches = cosmos_activity_logs_container.query_items(
67+
query=query,
68+
parameters=parameters,
69+
partition_key=user_id,
70+
)
71+
return next(iter(matches), None) is not None
72+
73+
3674
def coerce_activity_log_user_id(user_id: Any) -> str:
3775
"""Extract a stable string user id from a scalar or session-style identity payload."""
3876
if user_id is None:

‎application/single_app/functions_authentication.py‎

Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,47 @@ def decorated_function(*args, **kwargs):
959959
return "Forbidden", 403
960960
return decorated_function
961961

962+
def get_control_center_capabilities(user=None, settings=None):
963+
"""Return the Control Center permissions implied by the user's roles and settings."""
964+
user = session.get('user', {}) if user is None else user
965+
settings = get_settings() if settings is None else settings
966+
require_admin_role = settings.get("require_member_of_control_center_admin", False)
967+
require_dashboard_reader_role = settings.get(
968+
"require_member_of_control_center_dashboard_reader", False
969+
)
970+
971+
has_control_center_admin_role = (
972+
'roles' in user and 'ControlCenterAdmin' in user['roles']
973+
if isinstance(user, dict)
974+
else False
975+
)
976+
has_dashboard_reader_role = (
977+
'roles' in user and 'ControlCenterDashboardReader' in user['roles']
978+
if isinstance(user, dict)
979+
else False
980+
)
981+
has_regular_admin_role = (
982+
'roles' in user and 'Admin' in user['roles']
983+
if isinstance(user, dict)
984+
else False
985+
)
986+
987+
has_full_access = (
988+
has_control_center_admin_role if require_admin_role else has_regular_admin_role
989+
)
990+
can_view_dashboard = has_full_access or (
991+
require_dashboard_reader_role and has_dashboard_reader_role
992+
)
993+
return {
994+
'can_view_dashboard': bool(can_view_dashboard),
995+
'can_manage_users': bool(has_full_access),
996+
'can_manage_groups': bool(has_full_access),
997+
'can_manage_workspaces': bool(has_full_access),
998+
'can_view_activity_logs': bool(has_full_access),
999+
'can_run_maintenance': bool(has_full_access),
1000+
}
1001+
1002+
9621003
def control_center_required(access_level='admin'):
9631004
"""
9641005
Unified Control Center access control decorator.
@@ -984,48 +1025,34 @@ def decorated_function(*args, **kwargs):
9841025
user = session.get('user', {})
9851026
settings = get_settings()
9861027
require_member_of_control_center_admin = settings.get("require_member_of_control_center_admin", False)
987-
require_member_of_control_center_dashboard_reader = settings.get("require_member_of_control_center_dashboard_reader", False)
988-
989-
has_control_center_admin_role = 'roles' in user and 'ControlCenterAdmin' in user['roles']
990-
has_dashboard_reader_role = 'roles' in user and 'ControlCenterDashboardReader' in user['roles']
991-
has_regular_admin_role = 'roles' in user and 'Admin' in user['roles']
992-
993-
# Check if ControlCenterAdmin role requirement is enforced
994-
if require_member_of_control_center_admin:
995-
# ControlCenterAdmin role is REQUIRED for access
996-
# Only ControlCenterAdmin role grants full access
997-
if has_control_center_admin_role:
998-
return f(*args, **kwargs)
999-
1000-
# For dashboard access, check if DashboardReader role grants access
1001-
if access_level == 'dashboard':
1002-
if require_member_of_control_center_dashboard_reader and has_dashboard_reader_role:
1003-
return f(*args, **kwargs)
1004-
1005-
# User doesn't have ControlCenterAdmin role, deny access
1006-
# Note: Regular Admin role does NOT grant access when this setting is enabled
1007-
is_api_request = (request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html) or request.path.startswith('/api/')
1008-
if is_api_request:
1009-
return jsonify({"error": "Forbidden", "message": "Insufficient permissions (ControlCenterAdmin role required)"}), 403
1010-
else:
1011-
return "Forbidden: ControlCenterAdmin role required", 403
1012-
1013-
# ControlCenterAdmin requirement is NOT enforced (default behavior)
1014-
# Only regular Admin role grants access - ControlCenterAdmin role is IGNORED
1015-
if has_regular_admin_role:
1028+
capabilities = get_control_center_capabilities(user, settings)
1029+
permitted = (
1030+
capabilities['can_view_dashboard']
1031+
if access_level == 'dashboard'
1032+
else capabilities['can_manage_users']
1033+
)
1034+
if permitted:
10161035
return f(*args, **kwargs)
10171036

1018-
# For dashboard-only access, check if DashboardReader role is enabled and user has it
1019-
if access_level == 'dashboard':
1020-
if require_member_of_control_center_dashboard_reader and has_dashboard_reader_role:
1021-
return f(*args, **kwargs)
1022-
10231037
# User is not an admin and doesn't have special roles - deny access
10241038
is_api_request = (request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html) or request.path.startswith('/api/')
10251039
if is_api_request:
1026-
return jsonify({"error": "Forbidden", "message": "Insufficient permissions (Admin role required)"}), 403
1040+
required_role = (
1041+
"ControlCenterAdmin"
1042+
if require_member_of_control_center_admin
1043+
else "Admin"
1044+
)
1045+
return jsonify({
1046+
"error": "Forbidden",
1047+
"message": f"Insufficient permissions ({required_role} role required)",
1048+
}), 403
10271049
else:
1028-
return "Forbidden: Admin role required", 403
1050+
required_role = (
1051+
"ControlCenterAdmin"
1052+
if require_member_of_control_center_admin
1053+
else "Admin"
1054+
)
1055+
return f"Forbidden: {required_role} role required", 403
10291056
return decorated_function
10301057
return decorator
10311058

0 commit comments

Comments
 (0)