Skip to content
This repository has been archived by the owner on Nov 1, 2023. It is now read-only.

migrate webhooks #2254

Merged
merged 9 commits into from
Aug 16, 2022
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
49 changes: 49 additions & 0 deletions src/ApiService/ApiService/Functions/WebhookLogs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Net;

namespace Microsoft.OneFuzz.Service.Functions;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;

public class WebhookLogs {
private readonly ILogTracer _log;
private readonly IEndpointAuthorization _auth;
private readonly IOnefuzzContext _context;

public WebhookLogs(ILogTracer log, IEndpointAuthorization auth, IOnefuzzContext context) {
_log = log;
_auth = auth;
_context = context;
}

[Function("Webhook_logs")]
public Async.Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "POST", Route = "webhooks/logs")] HttpRequestData req) {
return _auth.CallIfUser(req, r => r.Method switch {
"POST" => Post(r),
_ => throw new InvalidOperationException("Unsupported HTTP method"),
});
}

private async Async.Task<HttpResponseData> Post(HttpRequestData req) {
var request = await RequestHandling.ParseRequest<WebhookGet>(req);
if (!request.IsOk) {
return await _context.RequestHandling.NotOk(
req,
request.ErrorV,
"webhook log");
}

var webhook = await _context.WebhookOperations.GetByWebhookId(request.OkV.WebhookId);

if (webhook == null) {
return await _context.RequestHandling.NotOk(req, new Error(ErrorCode.INVALID_REQUEST, new[] { "unable to find webhook" }), "webhook log");
}

_log.Info($"getting webhook logs: {request.OkV.WebhookId}");
var logs = _context.WebhookMessageLogOperations.SearchByPartitionKeys(new[] { $"{request.OkV.WebhookId}" });

var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(logs);
return response;
}
}
49 changes: 49 additions & 0 deletions src/ApiService/ApiService/Functions/WebhookPing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Net;

namespace Microsoft.OneFuzz.Service.Functions;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;

public class WebhookPing {
private readonly ILogTracer _log;
private readonly IEndpointAuthorization _auth;
private readonly IOnefuzzContext _context;

public WebhookPing(ILogTracer log, IEndpointAuthorization auth, IOnefuzzContext context) {
_log = log;
_auth = auth;
_context = context;
}

[Function("Webhook_ping")]
public Async.Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "POST", Route = "webhooks/ping")] HttpRequestData req) {
return _auth.CallIfUser(req, r => r.Method switch {
"POST" => Post(r),
_ => throw new InvalidOperationException("Unsupported HTTP method"),
});
}

private async Async.Task<HttpResponseData> Post(HttpRequestData req) {
var request = await RequestHandling.ParseRequest<WebhookGet>(req);
if (!request.IsOk) {
return await _context.RequestHandling.NotOk(
req,
request.ErrorV,
"webhook ping");
}

var webhook = await _context.WebhookOperations.GetByWebhookId(request.OkV.WebhookId);

if (webhook == null) {
return await _context.RequestHandling.NotOk(req, new Error(ErrorCode.INVALID_REQUEST, new[] { "unable to find webhook" }), "webhook ping");
}

_log.Info($"pinging webhook : {request.OkV.WebhookId}");
EventPing ping = await _context.WebhookOperations.Ping(webhook);

var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(ping);
return response;
}
}
140 changes: 140 additions & 0 deletions src/ApiService/ApiService/Functions/Webhooks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using System.Net;

namespace Microsoft.OneFuzz.Service.Functions;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;


public class Webhooks {
private readonly ILogTracer _log;
private readonly IEndpointAuthorization _auth;
private readonly IOnefuzzContext _context;

public Webhooks(ILogTracer log, IEndpointAuthorization auth, IOnefuzzContext context) {
_log = log;
_auth = auth;
_context = context;
}

[Function("Webhooks")]
public Async.Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", "POST", "DELETE", "PATCH")] HttpRequestData req) {
return _auth.CallIfUser(req, r => r.Method switch {
"GET" => Get(r),
"POST" => Post(r),
"DELETE" => Delete(r),
"PATCH" => Patch(r),
_ => throw new InvalidOperationException("Unsupported HTTP method"),
});
}


private async Async.Task<HttpResponseData> Get(HttpRequestData req) {
var request = await RequestHandling.ParseRequest<WebhookSearch>(req);
if (!request.IsOk) {
return await _context.RequestHandling.NotOk(req, request.ErrorV, "webhook get");
}

if (request.OkV.WebhookId != null) {
_log.Info($"getting webhook: {request.OkV.WebhookId}");
var webhook = await _context.WebhookOperations.GetByWebhookId(request.OkV.WebhookId.Value);

if (webhook == null) {
return await _context.RequestHandling.NotOk(req, new Error(ErrorCode.INVALID_REQUEST, new[] { "unable to find webhook" }), "webhook get");
}

var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(webhook);
return response;
chkeita marked this conversation as resolved.
Show resolved Hide resolved
}

_log.Info("listing webhooks");
var webhooks = _context.WebhookOperations.SearchAll().Select(w => w with { Url = null, SecretToken = null });

var response2 = req.CreateResponse(HttpStatusCode.OK);
await response2.WriteAsJsonAsync(webhooks);
return response2;


}


private async Async.Task<HttpResponseData> Patch(HttpRequestData req) {
var request = await RequestHandling.ParseRequest<WebhookUpdate>(req);
if (!request.IsOk) {
return await _context.RequestHandling.NotOk(
req,
request.ErrorV,
"webhook update");
}

_log.Info($"updating webhook: {request.OkV.WebhookId}");

var webhook = await _context.WebhookOperations.GetByWebhookId(request.OkV.WebhookId);

if (webhook == null) {
return await _context.RequestHandling.NotOk(req, new Error(ErrorCode.INVALID_REQUEST, new[] { "unable to find webhook" }), "webhook update");
}

var updated = webhook with {
Url = request.OkV.Url ?? webhook.Url,
Name = request.OkV.Name ?? webhook.Name,
EventTypes = request.OkV.EventTypes ?? webhook.EventTypes,
SecretToken = request.OkV.SecretToken ?? webhook.SecretToken,
MessageFormat = request.OkV.MessageFormat ?? webhook.MessageFormat
};

await _context.WebhookOperations.Replace(updated);

var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(updated with { Url = null, SecretToken = null });
return response;
}


private async Async.Task<HttpResponseData> Post(HttpRequestData req) {
var request = await RequestHandling.ParseRequest<WebhookCreate>(req);
if (!request.IsOk) {
return await _context.RequestHandling.NotOk(
req,
request.ErrorV,
"webhook create");
}

var webhook = new Webhook(Guid.NewGuid(), request.OkV.Name, request.OkV.Url, request.OkV.EventTypes,
request.OkV.SecretToken, request.OkV.MessageFormat);

await _context.WebhookOperations.Insert(webhook);

_log.Info($"added webhook: {webhook.WebhookId}");

var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(webhook with { Url = null, SecretToken = null });
return response;
}


private async Async.Task<HttpResponseData> Delete(HttpRequestData req) {
var request = await RequestHandling.ParseRequest<WebhookGet>(req);
if (!request.IsOk) {
return await _context.RequestHandling.NotOk(
req,
request.ErrorV,
context: "webhook delete");
}

_log.Info($"deleting webhook: {request.OkV.WebhookId}");

var webhook = await _context.WebhookOperations.GetByWebhookId(request.OkV.WebhookId);

if (webhook == null) {
return await _context.RequestHandling.NotOk(req, new Error(ErrorCode.INVALID_REQUEST, new[] { "unable to find webhook" }), "webhook delete");
}

await _context.WebhookOperations.Delete(webhook);

var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new BoolResult(true));
return response;
}
}
22 changes: 22 additions & 0 deletions src/ApiService/ApiService/OneFuzzTypes/Requests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,25 @@ public record PoolCreate(
bool Managed,
Guid? ClientId = null
);

public record WebhookCreate(
string Name,
Uri Url,
List<EventType> EventTypes,
string? SecretToken,
WebhookMessageFormat? MessageFormat
);


public record WebhookSearch(Guid? WebhookId);

public record WebhookGet(Guid WebhookId);

public record WebhookUpdate(
Guid WebhookId,
string? Name,
Uri? Url,
List<EventType>? EventTypes,
string? SecretToken,
WebhookMessageFormat? MessageFormat
);
2 changes: 1 addition & 1 deletion src/ApiService/ApiService/OneFuzzTypes/Webhooks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,6 @@ public record Webhook(
[RowKey] string Name,
Uri? Url,
List<EventType> EventTypes,
string SecretToken, // SecretString??
string? SecretToken, // SecretString??
WebhookMessageFormat? MessageFormat
) : EntityBase();
12 changes: 11 additions & 1 deletion src/ApiService/ApiService/onefuzzlib/WebhookOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
using System.Net.Http;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading.Tasks;
using ApiService.OneFuzzLib.Orm;
using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;

namespace Microsoft.OneFuzz.Service;

public interface IWebhookOperations {
public interface IWebhookOperations : IOrm<Webhook> {
Async.Task SendEvent(EventMessage eventMessage);
Async.Task<Webhook?> GetByWebhookId(Guid webhookId);
Async.Task<bool> Send(WebhookMessageLog messageLog);
Task<EventPing> Ping(Webhook webhook);
}

public class WebhookOperations : Orm<Webhook>, IWebhookOperations {
Expand Down Expand Up @@ -74,6 +76,14 @@ public async Async.Task<bool> Send(WebhookMessageLog messageLog) {
return false;
}

public async Task<EventPing> Ping(Webhook webhook) {
var ping = new EventPing(Guid.NewGuid());
var instanceId = await _context.Containers.GetInstanceId();
var instanceName = _context.Creds.GetInstanceName();
await AddEvent(webhook, new EventMessage(Guid.NewGuid(), EventType.Ping, ping, instanceId, instanceName));
return ping;
}

// Not converting to bytes, as it's not neccessary in C#. Just keeping as string.
public async Async.Task<Tuple<string, string?>> BuildMessage(Guid webhookId, Guid eventId, EventType eventType, BaseEvent webhookEvent, String? secretToken, WebhookMessageFormat? messageFormat) {
var entityConverter = new EntityConverter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public TableEntity ToTableEntity<T>(T typedEntity) where T : EntityBase {
if (prop.kind == EntityPropertyKind.PartitionKey || prop.kind == EntityPropertyKind.RowKey) {
return (prop.columnName, value?.ToString());
}
if (prop.type == typeof(Guid) || prop.type == typeof(Guid?)) {
if (prop.type == typeof(Guid) || prop.type == typeof(Guid?) || prop.type == typeof(Uri)) {
return (prop.columnName, value?.ToString());
}
if (prop.type == typeof(bool)
Expand Down
10 changes: 5 additions & 5 deletions src/ApiService/Tests/OrmTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ record Entity1(

[Fact]
public void TestBothDirections() {
var uriString = "https://localhost:9090";
var uriString = new Uri("https://localhost:9090");
var converter = new EntityConverter();
var entity1 = new Entity1(
Guid.NewGuid(),
Expand All @@ -71,7 +71,7 @@ public void TestBothDirections() {
TheEnumValue = TestEnumValue.Two
},
null,
new Uri(uriString),
uriString,
null
);

Expand Down Expand Up @@ -104,7 +104,7 @@ public void TestBothDirections() {

[Fact]
public void TestConvertToTableEntity() {
var uriString = "https://localhost:9090";
var uriString = new Uri("https://localhost:9090");
var converter = new EntityConverter();
var entity1 = new Entity1(
Guid.NewGuid(),
Expand All @@ -121,7 +121,7 @@ public void TestConvertToTableEntity() {
TheEnumValue = TestEnumValue.One
},
null,
new Uri(uriString),
uriString,
null
);
var tableEntity = converter.ToTableEntity(entity1);
Expand All @@ -136,7 +136,7 @@ public void TestConvertToTableEntity() {
Assert.Equal("flag_one,flag_two", tableEntity.GetString("the_flag"));
Assert.Equal("renamed", tableEntity.GetString("a__special__name"));

Assert.Equal(uriString, tableEntity.GetString("test_uri"));
Assert.Equal(uriString, new Uri(tableEntity.GetString("test_uri")));


var json = JsonNode.Parse(tableEntity.GetString("the_object"))?.AsObject() ?? throw new InvalidOperationException("Could not parse objec");
Expand Down