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
41 changes: 9 additions & 32 deletions application/single_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@
register_swagger_routes(app)

from flask_session import Session
from redis import Redis
import functions_redis_client
from functions_settings import get_settings
from functions_authentication import get_current_user_id
from functions_global_agents import ensure_default_global_agent_exists
Expand Down Expand Up @@ -221,41 +221,18 @@
try:
if redis_auth_type == 'managed_identity':
log_event("Redis enabled using Managed Identity", level=logging.INFO)
redis_client = app_settings_cache.create_redis_managed_identity_client(
redis_url,
settings=settings,
socket_connect_timeout=5,
socket_timeout=5
)
elif redis_auth_type == 'key_vault':
log_event("Redis enabled using Key Vault Secret", level=logging.INFO)
from functions_keyvault import retrieve_secret_direct
redis_key_secret_name = settings.get('redis_key', '').strip()
redis_password = retrieve_secret_direct(redis_key_secret_name)
if redis_password:
redis_password = redis_password.strip()
redis_client = Redis(
host=redis_url,
port=6380,
db=0,
password=redis_password,
ssl=True,
socket_connect_timeout=5,
socket_timeout=5
)
else:
redis_key = settings.get('redis_key', '').strip()
log_event("Redis enabled using Access Key", level=logging.INFO)
redis_client = Redis(
host=redis_url,
port=6380,
db=0,
password=redis_key,
ssl=True,
socket_connect_timeout=5,
socket_timeout=5
)


redis_client = functions_redis_client.create_redis_client(
settings=settings,
credential_purpose=functions_redis_client.CREDENTIAL_PURPOSE_SESSION,

Check warning on line 231 in application/single_app/app.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.
socket_connect_timeout=5,
socket_timeout=5
)

# Test the connection
redis_client.ping()
log_event("✅ Redis connection successful", level=logging.INFO)
Expand Down
114 changes: 24 additions & 90 deletions application/single_app/app_settings_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,24 @@
import json
import logging
import copy
import base64
import os
import threading
import time
from datetime import datetime, timedelta
from redis import Redis
from redis.credentials import CredentialProvider
from azure.identity import DefaultAzureCredential

# Redis client construction lives in functions_redis_client so session, cache, and admin
# diagnostics code paths share one place that resolves service type, port, and credentials.

Check warning on line 15 in application/single_app/app_settings_cache.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.
from functions_redis_client import (
AUTH_TYPE_MANAGED_IDENTITY,
CREDENTIAL_PURPOSE_APP_CACHE,

Check warning on line 18 in application/single_app/app_settings_cache.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.
create_redis_client,
)
Comment thread
paullizer marked this conversation as resolved.
Dismissed

# NOTE: functions_keyvault is imported locally inside configure_app_cache to avoid a circular
# import (functions_keyvault -> app_settings_cache -> functions_keyvault).
# functions_appinsights is also imported locally for the same reason.

_settings = None
_logger = logging.getLogger(__name__)
REDIS_ENTRA_TOKEN_SCOPE = 'https://redis.azure.com/.default'
REDIS_TOKEN_REFRESH_BUFFER_SECONDS = 300
APP_SETTINGS_CACHE = {}
APP_USER_UI_SETTINGS_CACHE = {}
APP_STREAM_SESSION_METADATA = {}
Expand Down Expand Up @@ -63,61 +64,16 @@
_app_cache_lock = threading.Lock()


def _get_redis_entra_token_scope(settings=None):
configured_scope = (settings or {}).get('redis_entra_token_scope') or os.getenv('REDIS_ENTRA_TOKEN_SCOPE')
return (configured_scope or REDIS_ENTRA_TOKEN_SCOPE).strip()


def _decode_token_claims(access_token):
parts = access_token.split('.')
if len(parts) < 2:
raise ValueError('Redis Microsoft Entra token did not contain JWT claims.')

payload = parts[1]
payload += '=' * (-len(payload) % 4)
decoded_payload = base64.urlsafe_b64decode(payload.encode('utf-8')).decode('utf-8')
return json.loads(decoded_payload)


def _get_redis_username_from_claims(access_token):
claims = _decode_token_claims(access_token)
username = claims.get('oid') or claims.get('appid')
if not username:
raise ValueError('Redis Microsoft Entra token did not include an object ID claim.')
return username


class RedisManagedIdentityCredentialProvider(CredentialProvider):
"""Provides Redis ACL username and Microsoft Entra token credentials."""

def __init__(self, credential=None, scope=None):
self.credential = credential or DefaultAzureCredential()
self.scope = scope or REDIS_ENTRA_TOKEN_SCOPE
self._cached_credentials = None
self._expires_on = 0

def get_credentials(self):
now = time.time()
if self._cached_credentials and now < self._expires_on - REDIS_TOKEN_REFRESH_BUFFER_SECONDS:
return self._cached_credentials

token = self.credential.get_token(self.scope)
username = _get_redis_username_from_claims(token.token)
self._cached_credentials = (username, token.token)
self._expires_on = token.expires_on
return self._cached_credentials


def create_redis_managed_identity_client(redis_url, settings=None, **redis_kwargs):
credential_provider = RedisManagedIdentityCredentialProvider(
scope=_get_redis_entra_token_scope(settings)
)
return Redis(
host=redis_url,
port=6380,
db=0,
credential_provider=credential_provider,
ssl=True,
"""Build a managed identity Redis client for the configured Azure Redis service.

Retained as a thin wrapper so existing callers keep working; the port, TLS, and
credential provider are resolved by functions_redis_client.

Check warning on line 71 in application/single_app/app_settings_cache.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.
"""
return create_redis_client(
settings=settings,
redis_url=redis_url,
auth_type=AUTH_TYPE_MANAGED_IDENTITY,
**redis_kwargs
)

Expand Down Expand Up @@ -707,39 +663,17 @@
raise ValueError('Redis cache is enabled but redis_url is empty.')
if redis_auth_type == 'managed_identity':
log_event("[ASC] Redis enabled using Managed Identity", level=logging.INFO)
redis_client = create_redis_managed_identity_client(
redis_url,
settings=settings
)
elif redis_auth_type == 'key_vault':
log_event("[ASC] Redis enabled using Key Vault Secret", level=logging.INFO)
# Local import to avoid circular dependency: functions_keyvault imports app_settings_cache.
from functions_keyvault import retrieve_secret_direct
redis_key_secret_name = settings.get('redis_key', '').strip()
# Pass settings directly: get_settings_cache() is still None at this point
# because configure_app_cache has not finished initialising the cache yet.
redis_password = retrieve_secret_direct(redis_key_secret_name, settings=settings)
if redis_password:
redis_password = redis_password.strip()
log_event("[ASC] Redis key retrieved from Key Vault successfully", level=logging.INFO)

redis_client = Redis(
host=redis_url,
port=6380,
db=0,
password=redis_password,
ssl=True
)
else:
redis_key = settings.get('redis_key', '').strip()
log_event("[ASC] Redis enabled using Access Key", level=logging.INFO)
redis_client = Redis(
host=redis_url,
port=6380,
db=0,
password=redis_key,
ssl=True
)

# Pass settings directly: get_settings_cache() is still None at this point

Check warning on line 671 in application/single_app/app_settings_cache.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.
# because configure_app_cache has not finished initialising the cache yet.
redis_client = create_redis_client(
settings=settings,
credential_purpose=CREDENTIAL_PURPOSE_APP_CACHE,

Check warning on line 675 in application/single_app/app_settings_cache.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.
)
app_cache_is_using_redis = True
APP_REDIS_CLIENT = redis_client
except Exception as redis_init_error:
Expand Down
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.009"
VERSION = "0.261.011"
IS_DEVELOPMENT = is_development_env_enabled()

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