Skip to content
This repository was archived by the owner on Nov 1, 2023. It is now read-only.
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
45 changes: 24 additions & 21 deletions src/ApiService/ApiService/onefuzzlib/ReproOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -327,30 +327,33 @@ public async Async.Task<Repro> SetError(Repro repro, Error result) {

public async Task<OneFuzzResult<Repro>> Create(ReproConfig config, UserInfo userInfo) {
var reportOrRegression = await _context.Reports.GetReportOrRegression(config.Container, config.Path);
if (reportOrRegression is Report report) {
var task = await _context.TaskOperations.GetByTaskId(report.TaskId);
if (task == null) {
return OneFuzzResult<Repro>.Error(ErrorCode.INVALID_REQUEST, "unable to find task");
}
if (reportOrRegression is not Report report) {
return OneFuzzResult<Repro>.Error(ErrorCode.UNABLE_TO_FIND, "unable to find report");
}

var vm = new Repro(
VmId: Guid.NewGuid(),
Config: config,
TaskId: task.TaskId,
Os: task.Os,
Auth: await Auth.BuildAuth(_logTracer),
EndTime: DateTimeOffset.UtcNow + TimeSpan.FromHours(config.Duration),
UserInfo: userInfo
);
var task = await _context.TaskOperations.GetByTaskId(report.TaskId);
if (task is null) {
return OneFuzzResult<Repro>.Error(ErrorCode.INVALID_REQUEST, "unable to find task");
}

var r = await _context.ReproOperations.Insert(vm);
if (!r.IsOk) {
_logTracer.WithHttpStatus(r.ErrorV).Error($"failed to insert repro record for {vm.VmId:Tag:VmId}");
}
return OneFuzzResult<Repro>.Ok(vm);
} else {
return OneFuzzResult<Repro>.Error(ErrorCode.UNABLE_TO_FIND, "unable to find report");
var vm = new Repro(
VmId: Guid.NewGuid(),
Config: config,
TaskId: task.TaskId,
Os: task.Os,
Auth: await Auth.BuildAuth(_logTracer),
EndTime: DateTimeOffset.UtcNow + TimeSpan.FromHours(config.Duration),
UserInfo: userInfo);

var r = await _context.ReproOperations.Insert(vm);
if (!r.IsOk) {
_logTracer.WithHttpStatus(r.ErrorV).Error($"failed to insert repro record for {vm.VmId:Tag:VmId}");
return OneFuzzResult<Repro>.Error(
ErrorCode.UNABLE_TO_CREATE,
new[] { "failed to insert repro record" });
}

return OneFuzzResult.Ok(vm);
}

public Task<Repro> ExtensionsFailed(Repro repro) {
Expand Down
8 changes: 4 additions & 4 deletions src/ApiService/ApiService/onefuzzlib/Request.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public interface IRequestHandling {
}

// See: https://www.rfc-editor.org/rfc/rfc7807#section-3
public sealed class ProblemDetails {
public sealed record ProblemDetails {
[JsonConstructor]
public ProblemDetails(int status, string title, string detail) {
Status = status;
Expand Down Expand Up @@ -45,14 +45,14 @@ public ProblemDetails(HttpStatusCode code, Error error) {
/// change from occurrence to occurrence of the problem, except for purposes
/// of localization (e.g., using proactive content negotiation; see
/// [RFC7231], Section 3.4).
public string Title { get; set; }
public string Title { get; }

/// The HTTP status code ([RFC7231], Section 6) generated by the origin
/// server for this occurrence of the problem.
public int Status { get; set; }
public int Status { get; }

// A human-readable explanation specific to this occurrence of the problem.
public string Detail { get; set; }
public string Detail { get; }
}

public class RequestHandling : IRequestHandling {
Expand Down
8 changes: 5 additions & 3 deletions src/ApiService/IntegrationTests/Fakes/TestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public TestContext(ILogTracer logTracer, IStorage storage, ICreds creds, string
ConfigOperations = new ConfigOperations(logTracer, this, cache);
PoolOperations = new PoolOperations(logTracer, this);
ScalesetOperations = new ScalesetOperations(logTracer, this);
ReproOperations = new ReproOperations(logTracer, this);
Reports = new Reports(logTracer, Containers);
UserCredentials = new UserCredentials(logTracer, ConfigOperations);
}

Expand All @@ -49,6 +51,7 @@ public Async.Task InsertAll(params EntityBase[] objs)
Node n => NodeOperations.Insert(n),
Pool p => PoolOperations.Insert(p),
Job j => JobOperations.Insert(j),
Repro r => ReproOperations.Insert(r),
NodeTasks nt => NodeTasksOperations.Insert(nt),
InstanceConfig ic => ConfigOperations.Insert(ic),
_ => throw new NotSupportedException($"You will need to add an TestContext.InsertAll case for {x.GetType()} entities"),
Expand Down Expand Up @@ -78,6 +81,8 @@ public Async.Task InsertAll(params EntityBase[] objs)
public IPoolOperations PoolOperations { get; }
public IScalesetOperations ScalesetOperations { get; }
public IVmssOperations VmssOperations { get; }
public IReproOperations ReproOperations { get; }
public IReports Reports { get; }
public EntityConverter EntityConverter { get; }

// -- Remainder not implemented --
Expand All @@ -100,9 +105,6 @@ public Async.Task InsertAll(params EntityBase[] objs)

public IProxyOperations ProxyOperations => throw new System.NotImplementedException();

public IReports Reports => throw new System.NotImplementedException();

public IReproOperations ReproOperations => throw new System.NotImplementedException();

public IScheduler Scheduler => throw new System.NotImplementedException();

Expand Down
249 changes: 249 additions & 0 deletions src/ApiService/IntegrationTests/ReproVmssTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.Json;
using IntegrationTests.Fakes;
using Microsoft.OneFuzz.Service;
using Microsoft.OneFuzz.Service.Functions;
using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
using Xunit;
using Xunit.Abstractions;
using Async = System.Threading.Tasks;

namespace IntegrationTests.Functions;

[Trait("Category", "Live")]
public class AzureStorageReproVmssTest : ReproVmssTestBase {
public AzureStorageReproVmssTest(ITestOutputHelper output)
: base(output, Integration.AzureStorage.FromEnvironment()) { }
}

public class AzuriteReproVmssTest : ReproVmssTestBase {
public AzuriteReproVmssTest(ITestOutputHelper output)
: base(output, new Integration.AzuriteStorage()) { }
}

public abstract class ReproVmssTestBase : FunctionTestBase {
public ReproVmssTestBase(ITestOutputHelper output, IStorage storage)
: base(output, storage) { }


[Theory]
[InlineData("POST", RequestType.Agent)]
[InlineData("POST", RequestType.NoAuthorization)]
[InlineData("GET", RequestType.Agent)]
[InlineData("GET", RequestType.NoAuthorization)]
[InlineData("DELETE", RequestType.Agent)]
[InlineData("DELETE", RequestType.NoAuthorization)]
public async Async.Task UserAuthorization_IsRequired(string method, RequestType authType) {
var auth = new TestEndpointAuthorization(authType, Logger, Context);
var func = new ReproVmss(Logger, auth, Context);
var result = await func.Run(TestHttpRequestData.Empty(method));
Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode);
}

[Fact]
public async Async.Task GetMissingVmFails() {
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ReproVmss(Logger, auth, Context);
var req = new ReproGet(VmId: Guid.NewGuid());
var result = await func.Run(TestHttpRequestData.FromJson("GET", req));
// TODO: should this be 404?
Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
var err = BodyAs<ProblemDetails>(result);
Assert.Equal("no such VM", err.Detail);
}

[Fact]
public async Async.Task GetAvailableVMsCanReturnEmpty() {
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ReproVmss(Logger, auth, Context);
var req = new ReproGet(VmId: null); // this means "all available"
var result = await func.Run(TestHttpRequestData.FromJson("GET", req));
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Empty(BodyAs<Repro[]>(result));
}

[Fact]
public async Async.Task GetAvailableVMsCanReturnVM() {
var vmId = Guid.NewGuid();

await Context.InsertAll(
new Repro(
VmId: vmId,
TaskId: Guid.NewGuid(),
new ReproConfig(Container.Parse("abcd"), "", 12345),
Auth: null,
Os: Os.Linux));

var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ReproVmss(Logger, auth, Context);
var req = new ReproGet(VmId: null); // this means "all available"
var result = await func.Run(TestHttpRequestData.FromJson("GET", req));
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
var repro = Assert.Single(BodyAs<Repro[]>(result));
Assert.Equal(vmId, repro.VmId);
}

[Fact]
public async Async.Task GetAvailableVMsCanReturnSpecificVM() {
var vmId = Guid.NewGuid();

await Context.InsertAll(
new Repro(
VmId: vmId,
TaskId: Guid.NewGuid(),
new ReproConfig(Container.Parse("abcd"), "", 12345),
Auth: null,
Os: Os.Linux));

var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ReproVmss(Logger, auth, Context);
var req = new ReproGet(VmId: vmId);
var result = await func.Run(TestHttpRequestData.FromJson("GET", req));
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(vmId, BodyAs<Repro>(result).VmId);
}


[Fact]
public async Async.Task GetAvailableVMsDoesNotReturnUnavailableVMs() {
await Context.InsertAll(
new Repro(
VmId: Guid.NewGuid(),
TaskId: Guid.NewGuid(),
new ReproConfig(Container.Parse("abcd"), "", 12345),
Auth: null,
Os: Os.Linux,
State: VmState.Stopping),
new Repro(
VmId: Guid.NewGuid(),
TaskId: Guid.NewGuid(),
new ReproConfig(Container.Parse("abcd"), "", 12345),
Auth: null,
Os: Os.Linux,
State: VmState.Stopped));

var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);
var func = new ReproVmss(Logger, auth, Context);
var req = new ReproGet(VmId: null); // this means "all available"
var result = await func.Run(TestHttpRequestData.FromJson("GET", req));
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Empty(BodyAs<Repro[]>(result));
}

[Fact]
public async Async.Task CannotCreateVMWithoutCredentials() {
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);

var func = new ReproVmss(Logger, auth, Context);
var req = new ReproCreate(Container.Parse("abcd"), "/", 12345);
var result = await func.Run(TestHttpRequestData.FromJson("POST", req));
Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
var err = BodyAs<ProblemDetails>(result);
Assert.Equal(new ProblemDetails(400, "INVALID_REQUEST", "unable to find authorization token"), err);
}

[Fact]
public async Async.Task CannotCreateVMForMissingReport() {
var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);

// setup fake user
var userInfo = new UserInfo(Guid.NewGuid(), Guid.NewGuid(), "upn");
Context.UserCredentials = new TestUserCredentials(Logger, Context.ConfigOperations, OneFuzzResult.Ok(userInfo));

var func = new ReproVmss(Logger, auth, Context);
var req = new ReproCreate(Container.Parse("abcd"), "/", 12345);
var result = await func.Run(TestHttpRequestData.FromJson("POST", req));
Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
var err = BodyAs<ProblemDetails>(result);
Assert.Equal(new ProblemDetails(400, "UNABLE_TO_FIND", "unable to find report"), err);
}

private async Async.Task<(Container, string)> CreateContainerWithReport(Guid jobId, Guid taskId) {
var container = Container.Parse(Guid.NewGuid().ToString("N"));
var filename = "report.json";
// Setup container with Report
var cc = GetContainerClient(container);
_ = await cc.CreateIfNotExistsAsync();
using (var ms = new MemoryStream()) {
var emptyReport = new Report(
null,
null,
"",
"",
"",
new List<string>(),
"",
"",
null,
TaskId: taskId,
JobId: jobId,
null,
null,
null,
null,
null,
null,
null,
null);

JsonSerializer.Serialize(ms, emptyReport, EntityConverter.GetJsonSerializerOptions());
_ = ms.Seek(0, SeekOrigin.Begin);
_ = await cc.UploadBlobAsync(filename, ms);
}

return (container, filename);
}

[Fact]
public async Async.Task CannotCreateVMForMissingTask() {
var (container, filename) = await CreateContainerWithReport(Guid.NewGuid(), Guid.NewGuid());

var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);

// setup fake user
var userInfo = new UserInfo(Guid.NewGuid(), Guid.NewGuid(), "upn");
Context.UserCredentials = new TestUserCredentials(Logger, Context.ConfigOperations, OneFuzzResult.Ok(userInfo));

var func = new ReproVmss(Logger, auth, Context);
var req = new ReproCreate(container, filename, 12345);
var result = await func.Run(TestHttpRequestData.FromJson("POST", req));
Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
var err = BodyAs<ProblemDetails>(result);
Assert.Equal(new ProblemDetails(400, "INVALID_REQUEST", "unable to find task"), err);
}

[Fact]
public async Async.Task CanCreateVMSuccessfully() {
// report must have TaskID pointing to a valid Task

var jobId = Guid.NewGuid();
var taskId = Guid.NewGuid();
var (container, filename) = await CreateContainerWithReport(jobId: jobId, taskId: taskId);
await Context.InsertAll(
new Task(
JobId: jobId,
TaskId: taskId,
TaskState.Running,
Os.Linux,
new TaskConfig(
JobId: jobId,
null,
new TaskDetails(TaskType.LibfuzzerFuzz, 12345))));

var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context);

// setup fake user
var userInfo = new UserInfo(Guid.NewGuid(), Guid.NewGuid(), "upn");
Context.UserCredentials = new TestUserCredentials(Logger, Context.ConfigOperations, OneFuzzResult.Ok(userInfo));

var func = new ReproVmss(Logger, auth, Context);
var req = new ReproCreate(container, filename, 12345);
var result = await func.Run(TestHttpRequestData.FromJson("POST", req));
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
var repro = BodyAs<Repro>(result);
Assert.Equal(taskId, repro.TaskId);
}
}