diff --git a/.github/instructions/location_of_feature_documentation.instructions.md b/.github/instructions/location_of_feature_documentation.instructions.md index ced7cb42..57a5c05f 100644 --- a/.github/instructions/location_of_feature_documentation.instructions.md +++ b/.github/instructions/location_of_feature_documentation.instructions.md @@ -7,7 +7,7 @@ applyTo: '**' ## Documentation Directory All new feature documentation should be placed in: ``` -..\docs\features\ +..\docs\explanation\features\ ``` ## File Naming Convention diff --git a/.github/instructions/location_of_fix_documentation.instructions.md b/.github/instructions/location_of_fix_documentation.instructions.md index f3eaee3a..311db387 100644 --- a/.github/instructions/location_of_fix_documentation.instructions.md +++ b/.github/instructions/location_of_fix_documentation.instructions.md @@ -7,7 +7,7 @@ applyTo: '**' ## Documentation Directory All bug fixes and issue resolution documentation should be placed in: ``` -..\docs\fixes\ +..\docs\explanation\fixes\ ``` ## File Naming Convention diff --git a/.github/workflows/python-syntax-check.yml b/.github/workflows/python-syntax-check.yml index ab656e60..34527de9 100644 --- a/.github/workflows/python-syntax-check.yml +++ b/.github/workflows/python-syntax-check.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - main + - Development paths: - 'application/single_app/**.py' - '.github/workflows/python-syntax-check.yml' diff --git a/README.md b/README.md index 3f5dcb76..31ea020b 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ azd env select This step will begin the deployment process. ```powershell -Use azd up +azd up ``` ## Architecture diff --git a/application/single_app/config.py b/application/single_app/config.py index 32f95593..650393e6 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -88,7 +88,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.235.003" +VERSION = "0.235.012" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/application/single_app/functions_authentication.py b/application/single_app/functions_authentication.py index f3fd1ce9..e4bcf480 100644 --- a/application/single_app/functions_authentication.py +++ b/application/single_app/functions_authentication.py @@ -731,10 +731,17 @@ def control_center_required(access_level='admin'): Args: access_level: 'admin' for full admin access, 'dashboard' for dashboard-only access - Access logic: + Access logic when require_member_of_control_center_admin is ENABLED: - ControlCenterAdmin role → Full access to everything (admin + dashboard) - - ControlCenterDashboardReader role → Dashboard access only - - Regular admins → Access when role requirements are disabled (default) + - ControlCenterDashboardReader role → Dashboard access only (if that setting is also enabled) + - Regular Admin role → NO access (must have ControlCenterAdmin) + - ControlCenterAdmin role is REQUIRED - having it without the setting enabled does nothing + + Access logic when require_member_of_control_center_admin is DISABLED (default): + - Regular Admin role → Full access to dashboard + management + activity logs + - ControlCenterAdmin role → IGNORED (role feature not enabled) + - ControlCenterDashboardReader role → Dashboard access only (if that setting is enabled) + - Non-admins → NO access """ def decorator(f): @wraps(f) @@ -744,37 +751,46 @@ def decorated_function(*args, **kwargs): 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_admin_role = 'roles' in user and 'ControlCenterAdmin' in user['roles'] + 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'] - # ControlCenterAdmin always has full access - if has_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) - - # Check if role requirements are enforced + # Check if ControlCenterAdmin role requirement is enforced if require_member_of_control_center_admin: - # Admin role required but user doesn't have it + # 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 - if access_level == 'dashboard' and require_member_of_control_center_dashboard_reader: - # Dashboard reader role required but user doesn't have it - 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 (ControlCenterDashboardReader role required)"}), 403 - else: - return "Forbidden: ControlCenterDashboardReader 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: + return f(*args, **kwargs) - # No role requirements enabled → allow all admins (default behavior) - 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 + else: + return "Forbidden: Admin role required", 403 return decorated_function return decorator diff --git a/application/single_app/route_frontend_admin_settings.py b/application/single_app/route_frontend_admin_settings.py index 8cdf2236..838a565c 100644 --- a/application/single_app/route_frontend_admin_settings.py +++ b/application/single_app/route_frontend_admin_settings.py @@ -593,7 +593,8 @@ def is_valid_url(url): # Workspaces 'enable_user_workspace': form_data.get('enable_user_workspace') == 'on', 'enable_group_workspaces': form_data.get('enable_group_workspaces') == 'on', - 'enable_group_creation': form_data.get('enable_group_creation') == 'on', + # disable_group_creation is inverted: when checked (on), enable_group_creation = False + 'enable_group_creation': form_data.get('disable_group_creation') != 'on', 'enable_public_workspaces': form_data.get('enable_public_workspaces') == 'on', 'enable_file_sharing': form_data.get('enable_file_sharing') == 'on', 'enable_file_processing_logs': enable_file_processing_logs, diff --git a/application/single_app/route_frontend_control_center.py b/application/single_app/route_frontend_control_center.py index 7215a60a..c5f3f44b 100644 --- a/application/single_app/route_frontend_control_center.py +++ b/application/single_app/route_frontend_control_center.py @@ -28,14 +28,30 @@ def control_center(): stats = get_control_center_statistics() # Check user's role for frontend conditional rendering + # Determine if user has full admin access (can see all tabs) user = session.get('user', {}) - has_admin_role = 'ControlCenterAdmin' in user.get('roles', []) + user_roles = user.get('roles', []) + require_member_of_control_center_admin = settings.get("require_member_of_control_center_admin", False) + + # User has full admin access based on which role requirement is active: + # - When require_member_of_control_center_admin is ENABLED: Only ControlCenterAdmin role grants access + # - When require_member_of_control_center_admin is DISABLED: Only regular Admin role grants access + has_control_center_admin_role = 'ControlCenterAdmin' in user_roles + has_regular_admin_role = 'Admin' in user_roles + + # Full admin access means they can see dashboard + management tabs + activity logs + if require_member_of_control_center_admin: + # ControlCenterAdmin role is required - only that role grants full access + has_full_admin_access = has_control_center_admin_role + else: + # ControlCenterAdmin requirement is disabled - only regular Admin role grants full access + has_full_admin_access = has_regular_admin_role return render_template('control_center.html', app_settings=public_settings, settings=public_settings, statistics=stats, - has_control_center_admin=has_admin_role) + has_control_center_admin=has_full_admin_access) except Exception as e: debug_print(f"Error loading control center: {e}") flash(f"Error loading control center: {str(e)}", "error") diff --git a/application/single_app/static/js/group/manage_group.js b/application/single_app/static/js/group/manage_group.js index 425e9ea1..371689db 100644 --- a/application/single_app/static/js/group/manage_group.js +++ b/application/single_app/static/js/group/manage_group.js @@ -1139,6 +1139,10 @@ function copyRawActivityToClipboard() { }); } +// Make functions globally available for onclick handlers +window.showRawActivity = showRawActivity; +window.copyRawActivityToClipboard = copyRawActivityToClipboard; + function showCsvError(message) { $("#csvErrorList").html(`
${escapeHtml(message)}
`); $("#csvErrorDetails").show(); diff --git a/application/single_app/templates/_sidebar_nav.html b/application/single_app/templates/_sidebar_nav.html index 8fd83cfa..531f4074 100644 --- a/application/single_app/templates/_sidebar_nav.html +++ b/application/single_app/templates/_sidebar_nav.html @@ -484,8 +484,11 @@ {% endif %} - - {% if request.endpoint == 'control_center' and ((session.get('user') and 'ControlCenterAdmin' in session['user']['roles']) or (app_settings.require_member_of_control_center_dashboard_reader and session.get('user') and 'ControlCenterDashboardReader' in session['user']['roles']) or ('Admin' in session['user']['roles'] and not app_settings.require_member_of_control_center_admin and not app_settings.require_member_of_control_center_dashboard_reader)) %} + + + + + {% if request.endpoint == 'control_center' and ((app_settings.require_member_of_control_center_admin and session.get('user') and 'ControlCenterAdmin' in session['user']['roles']) or (app_settings.require_member_of_control_center_dashboard_reader and session.get('user') and 'ControlCenterDashboardReader' in session['user']['roles']) or (not app_settings.require_member_of_control_center_admin and 'Admin' in session['user']['roles'])) %}
@@ -513,8 +516,10 @@ Dashboard - {# Only show admin tabs if user has ControlCenterAdmin role #} - {% if session.get('user') and 'ControlCenterAdmin' in session['user']['roles'] %} + {# Only show admin tabs if user has full admin access based on settings #} + {# When require_member_of_control_center_admin is ENABLED: need ControlCenterAdmin role #} + {# When DISABLED: need regular Admin role #} + {% if (app_settings.require_member_of_control_center_admin and session.get('user') and 'ControlCenterAdmin' in session['user']['roles']) or (not app_settings.require_member_of_control_center_admin and session.get('user') and 'Admin' in session['user']['roles']) %} {% endif %} - {# Control Center - accessible to admins OR users with ControlCenter roles #} - {% if (session.get('user') and 'ControlCenterAdmin' in session['user']['roles']) or (app_settings.require_member_of_control_center_dashboard_reader and session.get('user') and 'ControlCenterDashboardReader' in session['user']['roles']) or ('Admin' in session['user']['roles'] and not app_settings.require_member_of_control_center_admin and not app_settings.require_member_of_control_center_dashboard_reader) %} + {# Control Center - access based on role requirements #} + {# When require_member_of_control_center_admin ENABLED: only ControlCenterAdmin role grants access #} + {# When DISABLED (default): only regular Admin role grants access #} + {# DashboardReader role grants dashboard-only access when that setting is enabled #} + {% if (app_settings.require_member_of_control_center_admin and session.get('user') and 'ControlCenterAdmin' in session['user']['roles']) or (app_settings.require_member_of_control_center_dashboard_reader and session.get('user') and 'ControlCenterDashboardReader' in session['user']['roles']) or (not app_settings.require_member_of_control_center_admin and 'Admin' in session['user']['roles']) %}
  • Control Center
  • diff --git a/application/single_app/templates/_top_nav.html b/application/single_app/templates/_top_nav.html index db09b671..cd7e2258 100644 --- a/application/single_app/templates/_top_nav.html +++ b/application/single_app/templates/_top_nav.html @@ -191,8 +191,11 @@ App Settings {% endif %} - {# Control Center - accessible to admins OR users with ControlCenter roles #} - {% if (session.get('user') and 'ControlCenterAdmin' in session['user']['roles']) or (app_settings.require_member_of_control_center_dashboard_reader and session.get('user') and 'ControlCenterDashboardReader' in session['user']['roles']) or ('Admin' in session['user']['roles'] and not app_settings.require_member_of_control_center_admin and not app_settings.require_member_of_control_center_dashboard_reader) %} + {# Control Center - access based on role requirements #} + {# When require_member_of_control_center_admin ENABLED: only ControlCenterAdmin role grants access #} + {# When DISABLED (default): only regular Admin role grants access #} + {# DashboardReader role grants dashboard-only access when that setting is enabled #} + {% if (app_settings.require_member_of_control_center_admin and session.get('user') and 'ControlCenterAdmin' in session['user']['roles']) or (app_settings.require_member_of_control_center_dashboard_reader and session.get('user') and 'ControlCenterDashboardReader' in session['user']['roles']) or (not app_settings.require_member_of_control_center_admin and 'Admin' in session['user']['roles']) %}
  • Control Center
  • diff --git a/application/single_app/templates/control_center.html b/application/single_app/templates/control_center.html index 1dbe4ae8..6e954d67 100644 --- a/application/single_app/templates/control_center.html +++ b/application/single_app/templates/control_center.html @@ -541,10 +541,11 @@
    -
    + aria-label="Navigate to User Management. Total users: {{ statistics.total_users or 0 }}"{% else %}title="User Management (Admin access required)"{% endif %}>
    @@ -563,10 +564,11 @@
    -
    + aria-label="Navigate to Group Management. Total groups: {{ statistics.total_groups or 0 }}"{% else %}title="Group Management (Admin access required)"{% endif %}>
    @@ -585,10 +587,11 @@
    -
    + aria-label="Navigate to Public Workspaces Management. Total workspaces: {{ statistics.total_public_workspaces or 0 }}"{% else %}title="Public Workspaces Management (Admin access required)"{% endif %}>
    @@ -4688,5 +4691,18 @@
    ${title}
    } } }; + +// Initialize GroupManager when DOM is ready +document.addEventListener('DOMContentLoaded', function() { + // Small delay to ensure control-center.js has initialized window.controlCenter first + setTimeout(function() { + if (typeof GroupManager !== 'undefined' && GroupManager.init) { + GroupManager.init(); + console.log('GroupManager initialized successfully'); + } else { + console.error('GroupManager not available for initialization'); + } + }, 200); +}); {% endblock %} \ No newline at end of file diff --git a/docs/explanation/features/VIDEO_INDEXER_DUAL_AUTHENTICATION.md b/docs/explanation/features/VIDEO_INDEXER_DUAL_AUTHENTICATION.md deleted file mode 100644 index cd1d3ff6..00000000 --- a/docs/explanation/features/VIDEO_INDEXER_DUAL_AUTHENTICATION.md +++ /dev/null @@ -1,149 +0,0 @@ -# Video Indexer Dual Authentication Support - -## Feature Overview -Added comprehensive support for both API key and managed identity authentication methods for Azure Video Indexer integration. - -**Implemented in version:** 0.229.064 -**Fixed in version:** 0.229.065 - -## Background -Previously, the Video Indexer integration only supported managed identity authentication despite having API key fields in the admin UI. This feature implements full dual authentication support, allowing users to choose between: -- **Managed Identity**: Uses Azure ARM token-based authentication, generates access token via ARM API -- **API Key**: Uses subscription key to generate access token via Video Indexer auth endpoint - -## Technical Implementation - -### 1. Authentication Functions (`functions_authentication.py`) -- **New Function**: `get_video_indexer_account_token()` - Main entry point that branches based on authentication type -- **Enhanced Function**: `get_video_indexer_api_key_token()` - Uses API key to generate access token via Video Indexer auth endpoint -- **Enhanced Function**: `get_video_indexer_managed_identity_token()` - Handles ARM token acquisition and generates access token via ARM API - -#### Authentication Flow -```python -auth_type = settings.get("video_indexer_authentication_type", "managed_identity") - -if auth_type == "key": - return get_video_indexer_api_key_token(settings, video_id) -else: - return get_video_indexer_managed_identity_token(settings, video_id) -``` - -#### API Key Token Generation -```python -# Generate access token using API key -api_url = "https://api.videoindexer.ai" -auth_url = f"{api_url}/auth/{location}/Accounts/{account_id}/AccessToken" -headers = {"Ocp-Apim-Subscription-Key": api_key} -params = {"allowEdit": "true"} -response = requests.get(auth_url, headers=headers, params=params) -access_token = response.text.strip('"') -``` - -### 2. Video Processing Updates (`functions_documents.py`) -Updated all Video Indexer API calls to use access tokens for authentication: - -#### Authentication Pattern (Both Methods) -Both API key and managed identity authentication now return an access token that is used consistently across all Video Indexer API calls: -- Uses `accessToken` query parameter in all API requests -- No headers required for authentication after token is generated -- Token is generated once per operation and reused for upload, polling, and deletion - -#### API Key Flow -1. API key → Video Indexer auth endpoint → Access token -2. Access token → Video Indexer API calls (upload, poll, delete) - -#### Managed Identity Flow -1. Managed identity → ARM API → Access token -2. Access token → Video Indexer API calls (upload, poll, delete) - -#### Affected Operations -- Video upload and processing: `?accessToken={token}` -- Processing status polling: `?accessToken={token}` -- Video deletion: `?accessToken={token}` -- Video validation: Uses same access token pattern - -### 3. Admin UI Controls (`admin_settings.html`) -Added authentication type selector with conditional field visibility: - -#### New Controls -- **Authentication Type Dropdown**: Select between "Managed Identity" and "API Key" -- **Conditional Field Visibility**: - - API key field shown only when "API Key" selected - - ARM fields shown only when "Managed Identity" selected - -#### JavaScript Behavior -- Dynamic show/hide of relevant fields based on selection -- Seamless user experience with real-time form updates - -### 4. Backend Form Handling (`route_frontend_admin_settings.py`) -Updated form processing to capture and save the authentication type setting. - -### 5. Default Settings (`functions_settings.py`) -Added `video_indexer_authentication_type` with default value `"managed_identity"` to maintain backward compatibility. - -## Usage Instructions - -### Configuring API Key Authentication -1. Navigate to Admin Settings → Video Indexer -2. Select "API Key" from Authentication Type dropdown -3. Enter your Video Indexer subscription key -4. API key fields will be automatically shown -5. Save settings - -### Configuring Managed Identity Authentication -1. Navigate to Admin Settings → Video Indexer -2. Select "Managed Identity" from Authentication Type dropdown -3. Configure ARM resource management settings -4. Managed identity fields will be automatically shown -5. Save settings - -## Configuration Options - -### API Key Method -- **video_indexer_authentication_type**: `"key"` -- **video_indexer_key**: Your subscription key -- **video_indexer_account_id**: Your account ID -- **video_indexer_location**: Your region - -### Managed Identity Method -- **video_indexer_authentication_type**: `"managed_identity"` -- **video_indexer_arm_access_token**: Auto-acquired -- **video_indexer_account_id**: Your account ID -- **video_indexer_location**: Your region - -## Benefits -- **Flexibility**: Choose authentication method based on security requirements -- **Backward Compatibility**: Existing managed identity setups continue working -- **Security**: Support for both enterprise (managed identity) and development (API key) scenarios -- **User Experience**: Intuitive admin interface with contextual field visibility - -## Testing Coverage -Comprehensive functional testing validates: -- ✅ Settings configuration and defaults -- ✅ Authentication function branching logic -- ✅ Video processing API call adaptations -- ✅ Admin UI control behavior and visibility -- ✅ Backend form processing integration - -## Integration Points -- **Azure Video Indexer API**: Dual authentication support -- **Azure ARM API**: Managed identity token acquisition -- **Admin Settings UI**: Authentication method selection -- **Cosmos DB**: Settings persistence -- **Application Logging**: Authentication method tracking - -## Dependencies -- Azure Video Indexer service -- Azure ARM API (for managed identity) -- DefaultAzureCredential (for managed identity) -- Bootstrap CSS framework (for UI) - -## Known Limitations -- Authentication type cannot be changed during video processing -- API key method requires manual key management and rotation -- Managed identity requires proper Azure RBAC permissions - -## Future Enhancements -- Authentication method validation and testing within admin UI -- Automatic fallback between authentication methods -- Enhanced logging for authentication troubleshooting \ No newline at end of file diff --git a/docs/explanation/fixes/MULTIMODAL_VISION_SETTINGS_SAVE_FIX.md b/docs/explanation/fixes/MULTIMODAL_VISION_SETTINGS_SAVE_FIX.md deleted file mode 100644 index 9730a31e..00000000 --- a/docs/explanation/fixes/MULTIMODAL_VISION_SETTINGS_SAVE_FIX.md +++ /dev/null @@ -1,159 +0,0 @@ -# Multi-Modal Vision Settings Not Saving Fix - -**Version**: 0.229.090 -**Date**: November 21, 2025 -**Issue**: Multi-modal vision toggle and model selection reverted when saving admin settings - -## Problem - -When users enabled multi-modal vision analysis and selected a vision model in the admin settings, clicking "Save Settings" would cause the values to revert to their previous state. The settings appeared to save momentarily but would reset on page reload or when navigating away. - -**User Report**: "i tested the multi-modal model in app settings but when i click save it reverts so its not saving" - -## Root Cause - -The backend form processing code in `route_frontend_admin_settings.py` was not extracting and saving the new multi-modal vision fields from the form data. When the settings were saved to Cosmos DB, these fields were omitted: - -- `enable_multimodal_vision` (checkbox) -- `multimodal_vision_model` (dropdown selection) - -The HTML form had the correct `name` attributes: -```html - -