db620d33df
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
184 lines
6.2 KiB
C#
184 lines
6.2 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.TestHost;
|
|
using Microsoft.Extensions.AI;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Moq;
|
|
|
|
namespace Microsoft.Agents.AI.DevUI.UnitTests;
|
|
|
|
public class DevUIAccessControlTests
|
|
{
|
|
private static WebApplicationBuilder NewBuilder()
|
|
{
|
|
var builder = WebApplication.CreateBuilder();
|
|
builder.WebHost.UseTestServer();
|
|
|
|
var mockChatClient = new Mock<IChatClient>();
|
|
var agent = new ChatClientAgent(mockChatClient.Object, "Test", "agent-name");
|
|
builder.Services.AddKeyedSingleton<AIAgent>("agent-name", agent);
|
|
|
|
return builder;
|
|
}
|
|
|
|
private static void SimulateRemoteIp(WebApplication app, IPAddress remoteIp)
|
|
{
|
|
app.Use(async (HttpContext ctx, RequestDelegate next) =>
|
|
{
|
|
ctx.Connection.RemoteIpAddress = remoteIp;
|
|
await next(ctx);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NonLoopbackRequest_ReturnsForbiddenByDefaultAsync()
|
|
{
|
|
var builder = NewBuilder();
|
|
builder.Services.AddDevUI();
|
|
|
|
using var app = builder.Build();
|
|
SimulateRemoteIp(app, IPAddress.Parse("192.0.2.1"));
|
|
app.MapDevUI();
|
|
await app.StartAsync();
|
|
|
|
var response = await app.GetTestClient().GetAsync(new Uri("/v1/entities", UriKind.Relative));
|
|
|
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NonLoopbackRequest_IsAllowedWhenAllowRemoteAccessAsync()
|
|
{
|
|
var builder = NewBuilder();
|
|
builder.Services.AddDevUI(o => o.AllowRemoteAccess = true);
|
|
|
|
using var app = builder.Build();
|
|
SimulateRemoteIp(app, IPAddress.Parse("192.0.2.1"));
|
|
app.MapDevUI();
|
|
await app.StartAsync();
|
|
|
|
var response = await app.GetTestClient().GetAsync(new Uri("/v1/entities", UriKind.Relative));
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LoopbackRequest_WithAuthTokenSet_RequiresBearerHeaderAsync()
|
|
{
|
|
var builder = NewBuilder();
|
|
builder.Services.AddDevUI(o => o.AuthToken = "secret-token");
|
|
|
|
using var app = builder.Build();
|
|
SimulateRemoteIp(app, IPAddress.Loopback);
|
|
app.MapDevUI();
|
|
await app.StartAsync();
|
|
|
|
var response = await app.GetTestClient().GetAsync(new Uri("/v1/entities", UriKind.Relative));
|
|
|
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LoopbackRequest_WithCorrectBearerToken_SucceedsAsync()
|
|
{
|
|
var builder = NewBuilder();
|
|
builder.Services.AddDevUI(o => o.AuthToken = "secret-token");
|
|
|
|
using var app = builder.Build();
|
|
SimulateRemoteIp(app, IPAddress.Loopback);
|
|
app.MapDevUI();
|
|
await app.StartAsync();
|
|
|
|
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri("/v1/entities", UriKind.Relative));
|
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "secret-token");
|
|
var response = await app.GetTestClient().SendAsync(request);
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EnvironmentVariableToken_IsEnforcedWhenAuthTokenNotConfiguredAsync()
|
|
{
|
|
const string EnvVar = "DEVUI_AUTH_TOKEN";
|
|
const string EnvToken = "env-token";
|
|
var previous = Environment.GetEnvironmentVariable(EnvVar);
|
|
Environment.SetEnvironmentVariable(EnvVar, EnvToken);
|
|
|
|
WebApplication? app = null;
|
|
try
|
|
{
|
|
var builder = NewBuilder();
|
|
builder.Services.AddDevUI();
|
|
|
|
app = builder.Build();
|
|
|
|
// Force singleton construction so the env var is captured before we
|
|
// restore it; otherwise tests running in parallel can pick up the
|
|
// leaked DEVUI_AUTH_TOKEN.
|
|
_ = app.Services.GetRequiredService<DevUIAuthFilter>();
|
|
}
|
|
finally
|
|
{
|
|
Environment.SetEnvironmentVariable(EnvVar, previous);
|
|
}
|
|
|
|
await using (app)
|
|
{
|
|
SimulateRemoteIp(app, IPAddress.Loopback);
|
|
app.MapDevUI();
|
|
await app.StartAsync();
|
|
|
|
var missing = await app.GetTestClient().GetAsync(new Uri("/v1/entities", UriKind.Relative));
|
|
Assert.Equal(HttpStatusCode.Unauthorized, missing.StatusCode);
|
|
|
|
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri("/v1/entities", UriKind.Relative));
|
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", EnvToken);
|
|
var accepted = await app.GetTestClient().SendAsync(request);
|
|
Assert.Equal(HttpStatusCode.OK, accepted.StatusCode);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MetaEndpoint_IsReachableWithoutAuthenticationAsync()
|
|
{
|
|
var builder = NewBuilder();
|
|
builder.Services.AddDevUI(o => o.AuthToken = "secret-token");
|
|
|
|
using var app = builder.Build();
|
|
SimulateRemoteIp(app, IPAddress.Loopback);
|
|
app.MapDevUI();
|
|
await app.StartAsync();
|
|
|
|
var response = await app.GetTestClient().GetAsync(new Uri("/meta", UriKind.Relative));
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
Assert.Contains("\"auth_required\":true", body);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LoopbackRequest_WithWrongBearerToken_ReturnsUnauthorizedAsync()
|
|
{
|
|
var builder = NewBuilder();
|
|
builder.Services.AddDevUI(o => o.AuthToken = "secret-token");
|
|
|
|
using var app = builder.Build();
|
|
SimulateRemoteIp(app, IPAddress.Loopback);
|
|
app.MapDevUI();
|
|
await app.StartAsync();
|
|
|
|
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri("/v1/entities", UriKind.Relative));
|
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "not-the-token");
|
|
var response = await app.GetTestClient().SendAsync(request);
|
|
|
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
|
}
|
|
}
|