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
5 changes: 5 additions & 0 deletions dotnet/samples/Demos/CodeInterpreterPlugin/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ async Task<string> TokenProvider(CancellationToken cancellationToken)
sessionId: Guid.NewGuid().ToString(),
endpoint: new Uri(endpoint));

// Uncomment the following lines to enable file upload operations (disabled by default for security)
// settings.EnableDangerousFileUploads = true;
// settings.AllowedUploadDirectories = new[] { @"C:\allowed\upload\directory" };
// settings.AllowedDownloadDirectories = new[] { @"C:\allowed\download\directory" };

Console.WriteLine("=== Code Interpreter With Azure Container Apps Plugin Demo ===\n");

Console.WriteLine("Start your conversation with the assistant. Type enter or an empty message to quit.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -43,7 +44,11 @@ public SessionsPythonPluginTests()
this._settings = new(sessionId: Guid.NewGuid().ToString(), endpoint: new Uri(_spConfiguration.Endpoint))
{
CodeExecutionType = SessionsPythonSettings.CodeExecutionTypeSetting.Synchronous,
CodeInputType = SessionsPythonSettings.CodeInputTypeSetting.Inline
CodeInputType = SessionsPythonSettings.CodeInputTypeSetting.Inline,
// Enable file operations for integration tests
EnableDangerousFileUploads = true,
AllowedUploadDirectories = new[] { Path.GetFullPath("TestData") },
AllowedDownloadDirectories = new[] { Path.GetFullPath("TestData") }
};

this._httpClientFactory = new HttpClientFactory();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public async Task<SessionsPythonCodeExecutionResult> ExecuteCodeAsync(
/// <returns>The metadata of the uploaded file.</returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="HttpRequestException"></exception>
/// <exception cref="InvalidOperationException">Thrown when file operations are disabled or the path is not allowed.</exception>
[KernelFunction, Description("Uploads a file to the `/mnt/data` directory of the current session.")]
public async Task<SessionsRemoteFileMetadata> UploadFileAsync(
[Description("The name of the remote file, relative to `/mnt/data`.")] string remoteFileName,
Expand All @@ -122,11 +123,13 @@ public async Task<SessionsRemoteFileMetadata> UploadFileAsync(
Verify.NotNullOrWhiteSpace(remoteFileName, nameof(remoteFileName));
Verify.NotNullOrWhiteSpace(localFilePath, nameof(localFilePath));

this._logger.LogInformation("Uploading file: {LocalFilePath} to {RemoteFileName}", localFilePath, remoteFileName);
var validatedLocalPath = this.ValidateLocalPathForUpload(localFilePath);

this._logger.LogInformation("Uploading file: {LocalFilePath} to {RemoteFileName}", validatedLocalPath, remoteFileName);

using var httpClient = this._httpClientFactory.CreateClient();

using var fileContent = new ByteArrayContent(File.ReadAllBytes(localFilePath));
using var fileContent = new ByteArrayContent(File.ReadAllBytes(validatedLocalPath));

using var multipartFormDataContent = new MultipartFormDataContent()
{
Expand All @@ -147,27 +150,33 @@ public async Task<SessionsRemoteFileMetadata> UploadFileAsync(
/// <param name="localFilePath">The path to save the downloaded file to. If not provided won't save it in the disk.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The data of the downloaded file as byte array.</returns>
[KernelFunction, Description("Downloads a file from the `/mnt/data` directory of the current session.")]
/// <exception cref="InvalidOperationException">Thrown when file operations are disabled or the path is not allowed.</exception>
public async Task<byte[]> DownloadFileAsync(
[Description("The name of the remote file to download, relative to `/mnt/data`.")] string remoteFileName,
[Description("The path to save the downloaded file to. If not provided won't save it in the disk.")] string? localFilePath = null,
string remoteFileName,
string? localFilePath = null,
CancellationToken cancellationToken = default)
{
Verify.NotNullOrWhiteSpace(remoteFileName, nameof(remoteFileName));

this._logger.LogTrace("Downloading file: {RemoteFileName} to {LocalFileName}", remoteFileName, localFilePath);
string? validatedLocalPath = null;
if (!string.IsNullOrWhiteSpace(localFilePath))
{
validatedLocalPath = this.ValidateLocalPathForDownload(localFilePath);
}

this._logger.LogTrace("Downloading file: {RemoteFileName} to {LocalFileName}", remoteFileName, validatedLocalPath);

using var httpClient = this._httpClientFactory.CreateClient();

using var response = await this.SendAsync(httpClient, HttpMethod.Get, $"files/{Uri.EscapeDataString(remoteFileName)}/content", cancellationToken).ConfigureAwait(false);

var fileContent = await response.Content.ReadAsByteArrayAndTranslateExceptionAsync(cancellationToken).ConfigureAwait(false);

if (!string.IsNullOrWhiteSpace(localFilePath))
if (!string.IsNullOrWhiteSpace(validatedLocalPath))
{
try
{
File.WriteAllBytes(localFilePath, fileContent);
File.WriteAllBytes(validatedLocalPath, fileContent);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -279,6 +288,90 @@ private async Task<HttpResponseMessage> SendAsync(HttpClient httpClient, HttpMet
return await httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Validates that the local file path is within allowed upload directories.
/// </summary>
/// <param name="localFilePath">The local file path to validate.</param>
/// <returns>The canonicalized path if valid.</returns>
/// <exception cref="InvalidOperationException">Thrown when file operations are disabled or the path is not allowed.</exception>
private string ValidateLocalPathForUpload(string localFilePath)
{
if (!this._settings.EnableDangerousFileUploads)
{
throw new InvalidOperationException(
"File upload is disabled. Set 'EnableDangerousFileUploads' to true and configure 'AllowedUploadDirectories' to enable.");
}

if (this._settings.AllowedUploadDirectories is null || !this._settings.AllowedUploadDirectories.Any())
{
throw new InvalidOperationException(
"File upload requires 'AllowedUploadDirectories' to be configured.");
}

var canonicalPath = Path.GetFullPath(localFilePath);

foreach (var allowedDir in this._settings.AllowedUploadDirectories)
{
var canonicalAllowedDir = Path.GetFullPath(allowedDir);
// Ensure we match the directory correctly by appending separator
var separator = Path.DirectorySeparatorChar.ToString();
var allowedDirWithSeparator = canonicalAllowedDir.EndsWith(separator, StringComparison.OrdinalIgnoreCase)
? canonicalAllowedDir
: canonicalAllowedDir + separator;

if (canonicalPath.StartsWith(allowedDirWithSeparator, StringComparison.OrdinalIgnoreCase))
{
return canonicalPath;
}
}

throw new InvalidOperationException(
$"Access denied: '{localFilePath}' is not within allowed upload directories.");
}

/// <summary>
/// Validates that the local file path is within allowed download directories.
/// </summary>
/// <param name="localFilePath">The local file path to validate.</param>
/// <returns>The canonicalized path if valid.</returns>
/// <exception cref="InvalidOperationException">Thrown when the path is not allowed.</exception>
private string ValidateLocalPathForDownload(string localFilePath)
{
// If no restrictions configured, allow all paths (permissive by default for downloads)
if (this._settings.AllowedDownloadDirectories is null || !this._settings.AllowedDownloadDirectories.Any())
{
return Path.GetFullPath(localFilePath);
}

// Get the directory of the target file path
var targetDirectory = Path.GetDirectoryName(localFilePath);
if (string.IsNullOrEmpty(targetDirectory))
{
targetDirectory = ".";
}

var canonicalTargetDir = Path.GetFullPath(targetDirectory);
var canonicalFilePath = Path.GetFullPath(localFilePath);

foreach (var allowedDir in this._settings.AllowedDownloadDirectories)
{
var canonicalAllowedDir = Path.GetFullPath(allowedDir);
// Ensure we match the directory correctly by appending separator
var separator = Path.DirectorySeparatorChar.ToString();
var allowedDirWithSeparator = canonicalAllowedDir.EndsWith(separator, StringComparison.OrdinalIgnoreCase)
? canonicalAllowedDir
: canonicalAllowedDir + separator;

if (canonicalTargetDir.StartsWith(allowedDirWithSeparator, StringComparison.OrdinalIgnoreCase))
{
return canonicalFilePath;
}
}

throw new InvalidOperationException(
$"Access denied: '{localFilePath}' is not within allowed download directories.");
}

#if NET
[GeneratedRegex(@"^(\s|`)*(?i:python)?\s*", RegexOptions.ExplicitCapture)]
private static partial Regex RemoveLeadingWhitespaceBackticksPython();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,30 @@ public class SessionsPythonSettings
/// </summary>
public IEnumerable<string>? AllowedDomains { get; set; }

/// <summary>
/// Gets or sets a value indicating whether dangerous file upload operations are enabled.
/// Default is <c>false</c>. Must be set to <c>true</c> along with configuring
/// <see cref="AllowedUploadDirectories"/> to enable file uploads.
/// </summary>
[JsonIgnore]
public bool EnableDangerousFileUploads { get; set; }

/// <summary>
/// Gets or sets the list of allowed local directories for file uploads.
/// When <see cref="EnableDangerousFileUploads"/> is <c>true</c>, only files within these directories can be uploaded.
/// If <c>null</c> or empty, file uploads are denied.
/// </summary>
[JsonIgnore]
public IEnumerable<string>? AllowedUploadDirectories { get; set; }

/// <summary>
/// Gets or sets the list of allowed local directories for file downloads.
/// If configured, files can only be downloaded to these directories.
/// If <c>null</c> or empty, all paths are allowed (permissive by default).
/// </summary>
[JsonIgnore]
public IEnumerable<string>? AllowedDownloadDirectories { get; set; }

/// <summary>
/// The session identifier.
/// </summary>
Expand Down
Loading
Loading