Skip to content
Open
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 @@ -88,7 +88,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.236.007"
VERSION = "0.236.011"


SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
Expand Down
72 changes: 72 additions & 0 deletions application/single_app/functions_activity_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -1215,3 +1215,75 @@ def has_user_accepted_agreement_today(
)
debug_print(f"⚠️ Error checking user agreement acceptance: {str(e)}")
return False


def log_retention_policy_force_push(
admin_user_id: str,
admin_email: str,
scopes: list,
results: dict,
total_updated: int
) -> None:
"""
Log retention policy force push action to activity_logs container.

This creates a permanent audit record when an admin forces organization
default retention policies to be applied to all workspaces.

Args:
admin_user_id (str): User ID of the admin performing the force push
admin_email (str): Email of the admin performing the force push
scopes (list): List of workspace types affected (e.g., ['personal', 'group', 'public'])
results (dict): Breakdown of updates per workspace type
total_updated (int): Total number of workspaces/users updated
"""

try:
# Create force push activity record
force_push_activity = {
'id': str(uuid.uuid4()),
'user_id': admin_user_id, # Partition key
'activity_type': 'retention_policy_force_push',
'timestamp': datetime.utcnow().isoformat(),
'created_at': datetime.utcnow().isoformat(),
'admin': {
'user_id': admin_user_id,
'email': admin_email
},
'force_push_details': {
'scopes': scopes,
'results': results,
'total_updated': total_updated,
'executed_at': datetime.utcnow().isoformat()
},
'workspace_type': 'admin',
'workspace_context': {
'action': 'retention_policy_force_push'
}
}

# Save to activity_logs container for permanent audit trail
cosmos_activity_logs_container.create_item(body=force_push_activity)

# Also log to Application Insights for monitoring
log_event(
message=f"Retention policy force push executed by {admin_email} for scopes: {', '.join(scopes)}. Total updated: {total_updated}",
extra=force_push_activity,
level=logging.INFO
)

debug_print(f"✅ Retention policy force push logged: {scopes} by {admin_email}, updated {total_updated}")

except Exception as e:
# Log error but don't break the force push flow
log_event(
message=f"Error logging retention policy force push: {str(e)}",
extra={
'admin_user_id': admin_user_id,
'scopes': scopes,
'total_updated': total_updated,
'error': str(e)
},
level=logging.ERROR
)
debug_print(f"⚠️ Warning: Failed to log retention policy force push: {str(e)}")
83 changes: 74 additions & 9 deletions application/single_app/functions_retention_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,47 @@ def get_all_public_workspaces():
return []


def resolve_retention_value(value, workspace_type, retention_type, settings=None):
"""
Resolve a retention value, handling 'default' by looking up organization defaults.

Args:
value: The retention value ('none', 'default', or a number/string of days)
workspace_type: 'personal', 'group', or 'public'
retention_type: 'conversation' or 'document'
settings: Optional pre-loaded settings dict (to avoid repeated lookups)

Returns:
str or int: 'none' if no deletion, or the number of days as int
"""
if value is None or value == 'default' or value == '':
# Look up the organization default
if settings is None:
settings = get_settings()

setting_key = f'default_retention_{retention_type}_{workspace_type}'
default_value = settings.get(setting_key, 'none')

# If the org default is also 'none', return 'none'
if default_value == 'none' or default_value is None:
return 'none'

# Return the org default as the effective value
try:
return int(default_value)
except (ValueError, TypeError):
return 'none'

# User/workspace has their own explicit value
if value == 'none':
return 'none'

try:
return int(value)
except (ValueError, TypeError):
return 'none'


def execute_retention_policy(workspace_scopes=None, manual_execution=False):
"""
Execute retention policy for specified workspace scopes.
Expand Down Expand Up @@ -185,6 +226,9 @@ def process_personal_retention():
# Get all user settings
all_users = get_all_user_settings()

# Pre-load settings once for efficiency
settings = get_settings()

for user in all_users:
user_id = user.get('id')
if not user_id:
Expand All @@ -194,10 +238,15 @@ def process_personal_retention():
user_settings = user.get('settings', {})
retention_settings = user_settings.get('retention_policy', {})

conversation_retention_days = retention_settings.get('conversation_retention_days', 'none')
document_retention_days = retention_settings.get('document_retention_days', 'none')
# Get raw values (may be 'default', 'none', or a number)
raw_conversation_days = retention_settings.get('conversation_retention_days')
raw_document_days = retention_settings.get('document_retention_days')

# Skip if both are set to "none"
# Resolve to effective values (handles 'default' -> org default lookup)
conversation_retention_days = resolve_retention_value(raw_conversation_days, 'personal', 'conversation', settings)
document_retention_days = resolve_retention_value(raw_document_days, 'personal', 'document', settings)

# Skip if both resolve to "none"
if conversation_retention_days == 'none' and document_retention_days == 'none':
continue

Expand Down Expand Up @@ -273,6 +322,9 @@ def process_group_retention():
# Get all groups
all_groups = get_all_groups()

# Pre-load settings once for efficiency
settings = get_settings()

for group in all_groups:
group_id = group.get('id')
if not group_id:
Expand All @@ -281,10 +333,15 @@ def process_group_retention():
# Get group's retention settings
retention_settings = group.get('retention_policy', {})

conversation_retention_days = retention_settings.get('conversation_retention_days', 'none')
document_retention_days = retention_settings.get('document_retention_days', 'none')
# Get raw values (may be 'default', 'none', or a number)
raw_conversation_days = retention_settings.get('conversation_retention_days')
raw_document_days = retention_settings.get('document_retention_days')

# Resolve to effective values (handles 'default' -> org default lookup)
conversation_retention_days = resolve_retention_value(raw_conversation_days, 'group', 'conversation', settings)
document_retention_days = resolve_retention_value(raw_document_days, 'group', 'document', settings)

# Skip if both are set to "none"
# Skip if both resolve to "none"
if conversation_retention_days == 'none' and document_retention_days == 'none':
continue

Expand Down Expand Up @@ -359,6 +416,9 @@ def process_public_retention():
# Get all public workspaces
all_workspaces = get_all_public_workspaces()

# Pre-load settings once for efficiency
settings = get_settings()

for workspace in all_workspaces:
workspace_id = workspace.get('id')
if not workspace_id:
Expand All @@ -367,10 +427,15 @@ def process_public_retention():
# Get workspace's retention settings
retention_settings = workspace.get('retention_policy', {})

conversation_retention_days = retention_settings.get('conversation_retention_days', 'none')
document_retention_days = retention_settings.get('document_retention_days', 'none')
# Get raw values (may be 'default', 'none', or a number)
raw_conversation_days = retention_settings.get('conversation_retention_days')
raw_document_days = retention_settings.get('document_retention_days')

# Resolve to effective values (handles 'default' -> org default lookup)
conversation_retention_days = resolve_retention_value(raw_conversation_days, 'public', 'conversation', settings)
document_retention_days = resolve_retention_value(raw_document_days, 'public', 'document', settings)

# Skip if both are set to "none"
# Skip if both resolve to "none"
if conversation_retention_days == 'none' and document_retention_days == 'none':
continue

Expand Down
9 changes: 9 additions & 0 deletions application/single_app/functions_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,15 @@ def get_settings(use_cosmos=False):
'retention_conversation_max_days': 3650, # ~10 years
'retention_document_min_days': 1,
'retention_document_max_days': 3650, # ~10 years
# Default retention policies for each workspace type
# 'none' means no automatic deletion (users can still set their own)
# Numeric values (e.g., 30, 60, 90, 180, 365, 730) represent days
'default_retention_conversation_personal': 'none',
'default_retention_document_personal': 'none',
'default_retention_conversation_group': 'none',
'default_retention_document_group': 'none',
'default_retention_conversation_public': 'none',
'default_retention_document_public': 'none',
}

try:
Expand Down
4 changes: 2 additions & 2 deletions application/single_app/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ azure-monitor-query==1.4.1
Flask==2.2.5
Flask-WTF==1.2.1
gunicorn
Werkzeug==3.1.4
Werkzeug==3.1.5
requests==2.32.4
openai==1.67
docx2txt==0.8
Expand Down Expand Up @@ -49,7 +49,7 @@ azure-monitor-opentelemetry==1.6.13
psycopg2-binary==2.9.10
cython
pyyaml==6.0.2
aiohttp==3.12.15
aiohttp==3.13.3
html2text==2025.4.15
matplotlib==3.10.7
azure-cognitiveservices-speech==1.47.0
Loading