diff --git a/application/single_app/config.py b/application/single_app/config.py index 91288225..da63c230 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -94,7 +94,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.239.002" +VERSION = "0.239.004" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') @@ -176,12 +176,14 @@ def get_allowed_extensions(enable_video=False, enable_audio=False): # Add Support for Custom Azure Environments CUSTOM_GRAPH_URL_VALUE = os.getenv("CUSTOM_GRAPH_URL_VALUE", "") +CUSTOM_GRAPH_AUTHORITY_URL_VALUE = os.getenv("CUSTOM_GRAPH_AUTHORITY_URL_VALUE", "") CUSTOM_IDENTITY_URL_VALUE = os.getenv("CUSTOM_IDENTITY_URL_VALUE", "") CUSTOM_RESOURCE_MANAGER_URL_VALUE = os.getenv("CUSTOM_RESOURCE_MANAGER_URL_VALUE", "") CUSTOM_BLOB_STORAGE_URL_VALUE = os.getenv("CUSTOM_BLOB_STORAGE_URL_VALUE", "") CUSTOM_COGNITIVE_SERVICES_URL_VALUE = os.getenv("CUSTOM_COGNITIVE_SERVICES_URL_VALUE", "") CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE = os.getenv("CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE", "") CUSTOM_REDIS_CACHE_INFRASTRUCTURE_URL_VALUE = os.getenv("CUSTOM_REDIS_CACHE_INFRASTRUCTURE_URL_VALUE", "") +CUSTOM_OIDC_METADATA_URL_VALUE = os.getenv("CUSTOM_OIDC_METADATA_URL_VALUE", "") # Azure AD Configuration @@ -193,41 +195,39 @@ def get_allowed_extensions(enable_video=False, enable_audio=False): MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = os.getenv("MICROSOFT_PROVIDER_AUTHENTICATION_SECRET") LOGIN_REDIRECT_URL = os.getenv("LOGIN_REDIRECT_URL") HOME_REDIRECT_URL = os.getenv("HOME_REDIRECT_URL") # Front Door URL for home page - -OIDC_METADATA_URL = f"https://login.microsoftonline.com/{TENANT_ID}/v2.0/.well-known/openid-configuration" AZURE_ENVIRONMENT = os.getenv("AZURE_ENVIRONMENT", "public") # public, usgovernment, custom -if AZURE_ENVIRONMENT == "custom": +WORD_CHUNK_SIZE = 400 + +if AZURE_ENVIRONMENT == "custom" or CUSTOM_IDENTITY_URL_VALUE or CUSTOM_GRAPH_AUTHORITY_URL_VALUE: AUTHORITY = f"{CUSTOM_IDENTITY_URL_VALUE}/{TENANT_ID}" + authority = CUSTOM_GRAPH_AUTHORITY_URL_VALUE or CUSTOM_IDENTITY_URL_VALUE or AUTHORITY.rstrip(f'/{TENANT_ID}') elif AZURE_ENVIRONMENT == "usgovernment": AUTHORITY = f"https://login.microsoftonline.us/{TENANT_ID}" + authority = AzureAuthorityHosts.AZURE_GOVERNMENT else: AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}" + authority = AzureAuthorityHosts.AZURE_PUBLIC_CLOUD -WORD_CHUNK_SIZE = 400 - -if AZURE_ENVIRONMENT == "usgovernment": +if AZURE_ENVIRONMENT == "custom": + OIDC_METADATA_URL = CUSTOM_OIDC_METADATA_URL_VALUE or f"https://login.microsoftonline.com/{TENANT_ID}/v2.0/.well-known/openid-configuration" + resource_manager = CUSTOM_RESOURCE_MANAGER_URL_VALUE + video_indexer_endpoint = os.getenv("CUSTOM_VIDEO_INDEXER_ENDPOINT", "https://api.videoindexer.ai") + credential_scopes=[resource_manager + "/.default"] + cognitive_services_scope = CUSTOM_COGNITIVE_SERVICES_URL_VALUE + search_resource_manager = CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE + KEY_VAULT_DOMAIN = os.getenv("KEY_VAULT_DOMAIN", ".vault.azure.net") +elif AZURE_ENVIRONMENT == "usgovernment": OIDC_METADATA_URL = f"https://login.microsoftonline.us/{TENANT_ID}/v2.0/.well-known/openid-configuration" resource_manager = "https://management.usgovcloudapi.net" - authority = AzureAuthorityHosts.AZURE_GOVERNMENT credential_scopes=[resource_manager + "/.default"] cognitive_services_scope = "https://cognitiveservices.azure.us/.default" video_indexer_endpoint = "https://api.videoindexer.ai.azure.us" search_resource_manager = "https://search.azure.us" KEY_VAULT_DOMAIN = ".vault.usgovcloudapi.net" - -elif AZURE_ENVIRONMENT == "custom": - resource_manager = CUSTOM_RESOURCE_MANAGER_URL_VALUE - authority = CUSTOM_IDENTITY_URL_VALUE - video_indexer_endpoint = os.getenv("CUSTOM_VIDEO_INDEXER_ENDPOINT", "https://api.videoindexer.ai") - credential_scopes=[resource_manager + "/.default"] - cognitive_services_scope = CUSTOM_COGNITIVE_SERVICES_URL_VALUE - search_resource_manager = CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE - KEY_VAULT_DOMAIN = os.getenv("KEY_VAULT_DOMAIN", ".vault.azure.net") else: OIDC_METADATA_URL = f"https://login.microsoftonline.com/{TENANT_ID}/v2.0/.well-known/openid-configuration" resource_manager = "https://management.azure.com" - authority = AzureAuthorityHosts.AZURE_PUBLIC_CLOUD credential_scopes=[resource_manager + "/.default"] cognitive_services_scope = "https://cognitiveservices.azure.com/.default" video_indexer_endpoint = "https://api.videoindexer.ai" diff --git a/application/single_app/example.env b/application/single_app/example.env index 38318b48..7803e0b3 100644 --- a/application/single_app/example.env +++ b/application/single_app/example.env @@ -15,4 +15,9 @@ TENANT_ID="" SECRET_KEY="Generate-A-Strong-Random-Secret-Key-Here!" # AZURE_ENVIRONMENT: Set based on your cloud environment # Options: "public", "usgovernment", "custom" -AZURE_ENVIRONMENT="public" \ No newline at end of file +AZURE_ENVIRONMENT="public" + +# Optional Graph overrides (for cross-cloud identity/Graph scenarios) +# Example values: +# CUSTOM_GRAPH_URL_VALUE="https://graph.microsoft.com" +# CUSTOM_GRAPH_AUTHORITY_URL_VALUE="https://login.microsoftonline.com" \ No newline at end of file diff --git a/application/single_app/functions_authentication.py b/application/single_app/functions_authentication.py index e4bcf480..79487696 100644 --- a/application/single_app/functions_authentication.py +++ b/application/single_app/functions_authentication.py @@ -52,11 +52,12 @@ def _save_cache(cache): # Decide how to handle this, maybe clear cache or log extensively # session.pop("token_cache", None) # Option: Clear on serialization failure -def _build_msal_app(cache=None): +def _build_msal_app(cache=None, authority_override=None): """Builds the MSAL ConfidentialClientApplication, optionally initializing with a cache.""" + authority = authority_override or AUTHORITY return ConfidentialClientApplication( CLIENT_ID, - authority=AUTHORITY, + authority=authority, client_credential=CLIENT_SECRET, token_cache=cache # Pass the cache instance here ) @@ -88,7 +89,7 @@ def get_valid_access_token(scopes=None): required_scopes = scopes or SCOPE # Use default SCOPE if none provided - msal_app = _build_msal_app(cache=_load_cache()) + msal_app = _build_msal_app(cache=_load_cache(), authority_override=get_graph_authority()) user_info = session.get("user", {}) # MSAL uses home_account_id which combines oid and tid # Construct it carefully based on your id_token_claims structure @@ -160,7 +161,7 @@ def get_valid_access_token_for_plugins(scopes=None): required_scopes = scopes or SCOPE # Use default SCOPE if none provided - msal_app = _build_msal_app(cache=_load_cache()) + msal_app = _build_msal_app(cache=_load_cache(), authority_override=get_graph_authority()) user_info = session.get("user", {}) # MSAL uses home_account_id which combines oid and tid # Construct it carefully based on your id_token_claims structure @@ -844,6 +845,103 @@ def get_current_user_info(): "displayName": user.get("name") } + +def _normalize_authority(authority_base, tenant_id): + """Normalize an authority URL and append tenant when appropriate.""" + base = (authority_base or "").strip().rstrip("/") + tenant = (tenant_id or "").strip() + + if not base or not tenant: + return base + + lowered = base.lower() + tenant_lower = tenant.lower() + + if lowered.endswith(f"/{tenant_lower}"): + return base + + if lowered.endswith("/common") or lowered.endswith("/organizations") or lowered.endswith("/consumers"): + return base + + return f"{base}/{tenant}" + + +def get_graph_authority(): + """ + Resolve authority for Graph token acquisition, independent of general Azure environment defaults. + + Precedence: + 1. CUSTOM_GRAPH_AUTHORITY_URL_VALUE if provided + 2. Custom cloud identity authority for AZURE_ENVIRONMENT=custom + 3. Gov/Public cloud authority based on AZURE_ENVIRONMENT + """ + custom_graph_authority = (CUSTOM_GRAPH_AUTHORITY_URL_VALUE or "").strip() + if custom_graph_authority: + return _normalize_authority(custom_graph_authority, TENANT_ID) + + if AZURE_ENVIRONMENT == "custom": + return _normalize_authority(CUSTOM_IDENTITY_URL_VALUE, TENANT_ID) + + if AZURE_ENVIRONMENT == "usgovernment": + return f"https://login.microsoftonline.us/{TENANT_ID}" + + return f"https://login.microsoftonline.com/{TENANT_ID}" + + +def get_graph_base_url(): + """ + Resolve the Microsoft Graph base URL for this deployment. + + Precedence: + 1. CUSTOM_GRAPH_URL_VALUE if provided (works in any AZURE_ENVIRONMENT mode) + 2. Azure Gov Graph for usgovernment + 3. Public Graph by default + + Returns: + str: Normalized Graph base URL ending with /v1.0 + """ + custom_graph_url = (CUSTOM_GRAPH_URL_VALUE or "").strip().rstrip("/") + if custom_graph_url: + normalized = custom_graph_url + lowered = normalized.lower() + + # Allow legacy values such as https://.../v1.0/users + if lowered.endswith("/users"): + normalized = normalized[:-6].rstrip("/") + lowered = normalized.lower() + + if "/v1.0" not in lowered: + normalized = f"{normalized}/v1.0" + + return normalized + + if AZURE_ENVIRONMENT == "usgovernment": + return "https://graph.microsoft.us/v1.0" + + return "https://graph.microsoft.com/v1.0" + + +def get_graph_endpoint(path=""): + """ + Build a full Graph endpoint from a relative path. + + Args: + path (str): Relative Graph path (for example: "/users" or "users/{id}") + + Returns: + str: Fully qualified Microsoft Graph URL + """ + base_url = get_graph_base_url().rstrip("/") + path = (path or "").strip() + + if not path: + return base_url + + if not path.startswith("/"): + path = f"/{path}" + + return f"{base_url}{path}" + def get_user_profile_image(): """ Fetches the user's profile image from Microsoft Graph and returns it as base64. @@ -854,13 +952,7 @@ def get_user_profile_image(): debug_print("get_user_profile_image: Could not acquire access token") return None - # Determine the correct Graph endpoint based on Azure environment - if AZURE_ENVIRONMENT == "usgovernment": - profile_image_endpoint = "https://graph.microsoft.us/v1.0/me/photo/$value" - elif AZURE_ENVIRONMENT == "custom": - profile_image_endpoint = f"{CUSTOM_GRAPH_URL_VALUE}/me/photo/$value" - else: - profile_image_endpoint = "https://graph.microsoft.com/v1.0/me/photo/$value" + profile_image_endpoint = get_graph_endpoint("/me/photo/$value") headers = { "Authorization": f"Bearer {token}", diff --git a/application/single_app/route_backend_documents.py b/application/single_app/route_backend_documents.py index fb4eb19b..28d4ef69 100644 --- a/application/single_app/route_backend_documents.py +++ b/application/single_app/route_backend_documents.py @@ -1378,7 +1378,7 @@ def api_get_shared_users(document_id): approval_status = entry.get('approval_status', 'unknown') try: # Get user details from Microsoft Graph - graph_url = f"https://graph.microsoft.com/v1.0/users/{oid}" + graph_url = get_graph_endpoint(f"/users/{oid}") response = requests.get(graph_url, headers=headers) if response.status_code == 200: diff --git a/application/single_app/route_backend_public_workspaces.py b/application/single_app/route_backend_public_workspaces.py index ffe679eb..6d14f357 100644 --- a/application/single_app/route_backend_public_workspaces.py +++ b/application/single_app/route_backend_public_workspaces.py @@ -44,12 +44,7 @@ def get_user_details_from_graph(user_id): if not token: return {"displayName": "", "email": ""} - if AZURE_ENVIRONMENT == "usgovernment": - user_endpoint = f"https://graph.microsoft.us/v1.0/users/{user_id}" - elif AZURE_ENVIRONMENT == "custom": - user_endpoint = f"{CUSTOM_GRAPH_URL_VALUE}/{user_id}" - else: - user_endpoint = f"https://graph.microsoft.com/v1.0/users/{user_id}" + user_endpoint = get_graph_endpoint(f"/users/{user_id}") headers = { "Authorization": f"Bearer {token}", diff --git a/application/single_app/route_backend_users.py b/application/single_app/route_backend_users.py index d2ca52f8..ad3ae5f4 100644 --- a/application/single_app/route_backend_users.py +++ b/application/single_app/route_backend_users.py @@ -24,12 +24,7 @@ def api_user_search(): if not token: return jsonify({"error": "Could not acquire access token"}), 401 - if AZURE_ENVIRONMENT == "usgovernment": - user_endpoint = "https://graph.microsoft.us/v1.0/users" - elif AZURE_ENVIRONMENT == "custom": - user_endpoint = CUSTOM_GRAPH_URL_VALUE - else: - user_endpoint = "https://graph.microsoft.com/v1.0/users" + user_endpoint = get_graph_endpoint("/users") headers = { "Authorization": f"Bearer {token}",