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
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.309"
VERSION = "0.261.311"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
1 change: 1 addition & 0 deletions application/single_app/functions_file_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,7 @@
},
"limits": {"max_sources": config["file_sync_max_sources_per_scope"]},
"recursive_allowed": bool(config["file_sync_allow_recursive_sources"]),
"default_remote_delete_policy": config["file_sync_default_remote_delete_policy"],

Check warning on line 870 in application/single_app/functions_file_sync.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.
}


Expand Down Expand Up @@ -3039,7 +3040,7 @@

def _graph_get_json(path_or_url: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
url = path_or_url if str(path_or_url or "").startswith("http") else get_graph_endpoint(path_or_url)
response = requests.get(url, headers=_onedrive_headers(), params=params, timeout=30)

Check failure

Code scanning / CodeQL

Partial server-side request forgery Critical

Part of the URL of this request depends on a
user-provided value
.
Part of the URL of this request depends on a user-provided value.
Part of the URL of this request depends on a user-provided value.
if response.status_code >= 400:
try:
error_body = response.json()
Expand Down Expand Up @@ -3270,7 +3271,7 @@
credential = DefaultAzureCredential(managed_identity_client_id=auth.get("managed_identity_client_id") or None)
else:
raise ValueError("Azure Files sources require managed identity, service principal, or connection string authentication")
return ShareServiceClient(account_url=account_url, credential=credential)

Check failure

Code scanning / CodeQL

Partial server-side request forgery Critical

Part of the URL of this request depends on a
user-provided value
.
Part of the URL of this request depends on a user-provided value.
Part of the URL of this request depends on a user-provided value.


def _get_azure_files_share_client(source: Dict[str, Any]):
Expand Down
135 changes: 116 additions & 19 deletions application/single_app/route_backend_file_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import logging

from flask import jsonify, request
from flask import jsonify, request, session

from functions_appinsights import log_event
from functions_authentication import admin_required, enabled_required, get_current_user_id, get_current_user_info, login_required, user_required
Expand All @@ -12,10 +12,15 @@
FILE_SYNC_SCOPE_PERSONAL,
FILE_SYNC_SCOPE_PUBLIC,
FILE_SYNC_SOURCE_TYPE_SMB,
FileSyncConfigConflict,
FileSyncDeleteIncomplete,
FileSyncPublicValidationError,
FileSyncSourceBusy,
FileSyncWriteConflict,
assert_public_workspace_role,
browse_file_sync_source_path,
build_file_sync_source_options,
compute_file_sync_config_revision,
create_file_sync_source,
delete_file_sync_source,
get_authorized_sync_source,
Expand Down Expand Up @@ -51,7 +56,9 @@
user_id = get_current_user_id()
if not user_id:
return None, None
return user_id, get_current_user_info() or {}
user_info = get_current_user_info() or {}
user_info["roles"] = (session.get("user") or {}).get("roles", [])

Check warning on line 60 in application/single_app/route_backend_file_sync.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.
return user_id, user_info

def _require_personal_context():
user_id, user_info = _current_user()
Expand Down Expand Up @@ -121,57 +128,118 @@
}

def _map_exception(error):
expected_error = isinstance(error, (PermissionError, LookupError, ValueError, FileSyncWriteConflict))
expected_error = isinstance(error, (
PermissionError, LookupError, ValueError, FileSyncWriteConflict,

Check warning on line 132 in application/single_app/route_backend_file_sync.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.
FileSyncConfigConflict, FileSyncSourceBusy,
))
log_event(
"[FILE_SYNC] Request failed.",
level=logging.WARNING if expected_error else logging.ERROR,
extra={
"endpoint": request.endpoint or "",
"method": request.method,
"exception_type": type(error).__name__,
"error": str(error),
},
exceptionTraceback=not expected_error,
)
if isinstance(error, FileSyncPublicValidationError):
return _error(error.public_message, 400)
partial_result = getattr(error, "delete_result", None)

Check warning on line 147 in application/single_app/route_backend_file_sync.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
partial_fields = (
{"partial": bool(getattr(error, "partial", False)), "delete_result": partial_result}

Check warning on line 149 in application/single_app/route_backend_file_sync.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
if partial_result is not None else {}
)
if isinstance(error, FileSyncDeleteIncomplete):
return jsonify({
"error": error.public_message,
"error_code": "delete_incomplete",
"delete_result": error.delete_result,
"partial": error.delete_result.get("documents_deleted", 0) > 0,
}), 409
if isinstance(error, FileSyncConfigConflict):
return jsonify({
"error": "This file source changed while it was being saved. Reload it and try again.",
"error_code": "config_conflict", **partial_fields,
}), 409
if isinstance(error, FileSyncSourceBusy):
return jsonify({
"error": "Wait for the running sync to finish, then delete the source.",
"error_code": "source_busy", **partial_fields,
}), 409
if isinstance(error, FileSyncWriteConflict):
return _error("This File Sync item changed while it was being saved. Reload it and try again.", 409)
return jsonify({
"error": "This File Sync item changed while it was being saved. Try saving again.",
"error_code": "write_conflict", **partial_fields,
}), 409
if isinstance(error, PermissionError):
return _error("You do not have permission to perform this File Sync operation.", 403)
if isinstance(error, LookupError):
if partial_fields:
return jsonify({
"error": "The requested File Sync resource was not found.", **partial_fields,
}), 404
return _error("The requested File Sync resource was not found.", 404)
if isinstance(error, ValueError):
return _error("The File Sync request could not be completed. Verify the source configuration and try again.", 400)
return _error("An unexpected error occurred while processing the File Sync request.", 500)

def _project_source(scope_type, source):
projected = sanitize_file_sync_source(source)
if scope_type == FILE_SYNC_SCOPE_PERSONAL:
for field in ("_etag", "_rid", "_self", "_attachments", "_ts"):
projected.pop(field, None)
projected["config_revision"] = compute_file_sync_config_revision(source)
return projected

def _list_sources(scope_type, scope_id):
sources = [sanitize_file_sync_source(source) for source in list_file_sync_sources(scope_type, scope_id)]
sources = [_project_source(scope_type, source) for source in list_file_sync_sources(scope_type, scope_id)]
return jsonify({"sources": sources}), 200

def _expected_revision(payload):
if "expected_config_revision" not in payload:
return None
revision = payload.pop("expected_config_revision")
if not isinstance(revision, str) or not revision.strip():
raise FileSyncPublicValidationError("Provide a valid configuration revision.")
return revision.strip()

def _personal_write_payload():
payload = _payload()
if not isinstance(payload, dict):
raise FileSyncPublicValidationError("A JSON object is required for this action.")
return dict(payload)

def _assert_new_source_type_visible(payload):
source_type = str(payload.get("source_type") or FILE_SYNC_SOURCE_TYPE_SMB).strip().lower()
if not is_file_sync_source_type_visible(get_settings(), source_type):
raise PermissionError("This File Sync source type is not available")

def _create_source(scope_type, scope_id, user_id):
payload = _payload()
def _create_source(scope_type, scope_id, user_id, *, personal_editor=False):
payload = _personal_write_payload() if personal_editor else _payload()
_assert_new_source_type_visible(payload)
source = create_file_sync_source(scope_type, scope_id, payload, user_id)
return jsonify({"source": sanitize_file_sync_source(source)}), 201

def _update_source(scope_type, scope_id, source_id, user_id):
source = update_file_sync_source(scope_type, scope_id, source_id, _payload(), user_id)
return jsonify({"source": sanitize_file_sync_source(source)}), 200
source = create_file_sync_source(scope_type, scope_id, payload, user_id, stage_secrets=personal_editor)

Check warning on line 220 in application/single_app/route_backend_file_sync.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 jsonify({"source": _project_source(scope_type, source)}), 201

def _update_source(scope_type, scope_id, source_id, user_id, *, personal_editor=False):
payload = _personal_write_payload() if personal_editor else _payload()
revision = _expected_revision(payload) if personal_editor else None
source = update_file_sync_source(
scope_type, scope_id, source_id, payload, user_id,
expected_config_revision=revision, stage_secrets=revision is not None,

Check warning on line 228 in application/single_app/route_backend_file_sync.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 jsonify({"source": _project_source(scope_type, source)}), 200

def _delete_source(scope_type, scope_id, source_id, user_id):
payload = _payload()
def _delete_source(scope_type, scope_id, source_id, user_id, *, personal_editor=False):
payload = _personal_write_payload() if personal_editor else _payload()
revision = _expected_revision(payload) if personal_editor else None
delete_result = delete_file_sync_source(
scope_type,
scope_id,
source_id,
user_id,
delete_associated_files=bool(payload.get("delete_associated_files")),
expected_config_revision=revision,
refuse_active_run=revision is not None,
)
return jsonify({"message": "File Sync source deleted", "delete_result": delete_result}), 200

Expand Down Expand Up @@ -571,6 +639,35 @@
except Exception as error:
return _map_exception(error)

@bp.route('/api/file-sync/personal/source-options', methods=['GET'])
@swagger_route(security=get_auth_security())

Check warning on line 643 in application/single_app/route_backend_file_sync.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.
@login_required

Check warning on line 644 in application/single_app/route_backend_file_sync.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.
@user_required

Check warning on line 645 in application/single_app/route_backend_file_sync.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.
@enabled_required("enable_file_sync")
def api_file_sync_personal_source_options():
try:
user_id = _require_personal_context()
if request.args or request.get_data():
raise FileSyncPublicValidationError("This request does not accept parameters or a body.")
return jsonify(build_file_sync_source_options(FILE_SYNC_SCOPE_PERSONAL, user_id, get_settings())), 200
except Exception as error:
return _map_exception(error)

@bp.route('/api/file-sync/personal/sources/<source_id>', methods=['GET'])
@swagger_route(security=get_auth_security())
@login_required
@user_required
@enabled_required("enable_file_sync")
def api_file_sync_personal_source_read(source_id):
try:
user_id = _require_personal_context()
if request.args or request.get_data():
raise FileSyncPublicValidationError("This request does not accept parameters or a body.")
source = get_authorized_sync_source(FILE_SYNC_SCOPE_PERSONAL, source_id, user_id, scope_id=user_id)
return jsonify({"source": _project_source(FILE_SYNC_SCOPE_PERSONAL, source)}), 200
except Exception as error:
return _map_exception(error)

@bp.route('/api/file-sync/<scope_type>/sources/browse', methods=['POST'])
@swagger_route(security=get_auth_security())
@login_required
Expand Down Expand Up @@ -637,7 +734,7 @@
def api_file_sync_personal_sources_create():
try:
user_id = _require_personal_context()
return _create_source(FILE_SYNC_SCOPE_PERSONAL, user_id, user_id)
return _create_source(FILE_SYNC_SCOPE_PERSONAL, user_id, user_id, personal_editor=True)
except Exception as error:
return _map_exception(error)

Expand All @@ -661,7 +758,7 @@
def api_file_sync_personal_source_update(source_id):
try:
user_id = _require_personal_context()
return _update_source(FILE_SYNC_SCOPE_PERSONAL, user_id, source_id, user_id)
return _update_source(FILE_SYNC_SCOPE_PERSONAL, user_id, source_id, user_id, personal_editor=True)
except Exception as error:
return _map_exception(error)

Expand All @@ -685,7 +782,7 @@
def api_file_sync_personal_source_delete(source_id):
try:
user_id = _require_personal_context()
return _delete_source(FILE_SYNC_SCOPE_PERSONAL, user_id, source_id, user_id)
return _delete_source(FILE_SYNC_SCOPE_PERSONAL, user_id, source_id, user_id, personal_editor=True)
except Exception as error:
return _map_exception(error)

Expand Down
Loading
Loading