From 1e09ad49385aabed8bc9050b349da5dc2e5601c1 Mon Sep 17 00:00:00 2001 From: Cheick Keita Date: Sun, 14 Aug 2022 21:15:40 -0700 Subject: [PATCH 1/5] migrate webhooks --- .../ApiService/Functions/Webhooks.cs | 141 ++++++++++++++++++ .../ApiService/OneFuzzTypes/Requests.cs | 22 +++ .../ApiService/OneFuzzTypes/Webhooks.cs | 2 +- .../onefuzzlib/WebhookOperations.cs | 2 +- 4 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 src/ApiService/ApiService/Functions/Webhooks.cs diff --git a/src/ApiService/ApiService/Functions/Webhooks.cs b/src/ApiService/ApiService/Functions/Webhooks.cs new file mode 100644 index 0000000000..5e35e614f6 --- /dev/null +++ b/src/ApiService/ApiService/Functions/Webhooks.cs @@ -0,0 +1,141 @@ +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 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 Get(HttpRequestData req) { + var request = await RequestHandling.ParseRequest(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; + } + + _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 Patch(HttpRequestData req) { + var request = await RequestHandling.ParseRequest(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 Post(HttpRequestData req) { + var request = await RequestHandling.ParseRequest(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 Delete(HttpRequestData req) { + var request = await RequestHandling.ParseRequest(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; + } +} diff --git a/src/ApiService/ApiService/OneFuzzTypes/Requests.cs b/src/ApiService/ApiService/OneFuzzTypes/Requests.cs index 824c5abe61..8f040ae4c5 100644 --- a/src/ApiService/ApiService/OneFuzzTypes/Requests.cs +++ b/src/ApiService/ApiService/OneFuzzTypes/Requests.cs @@ -189,3 +189,25 @@ public record PoolCreate( bool Managed, Guid? ClientId = null ); + +public record WebhookCreate( + string Name, + Uri Url, + List 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? EventTypes, + string? SecretToken, + WebhookMessageFormat? MessageFormat +); diff --git a/src/ApiService/ApiService/OneFuzzTypes/Webhooks.cs b/src/ApiService/ApiService/OneFuzzTypes/Webhooks.cs index bcf159e165..7b3555ae6d 100644 --- a/src/ApiService/ApiService/OneFuzzTypes/Webhooks.cs +++ b/src/ApiService/ApiService/OneFuzzTypes/Webhooks.cs @@ -50,6 +50,6 @@ public record Webhook( [RowKey] string Name, Uri? Url, List EventTypes, - string SecretToken, // SecretString?? + string? SecretToken, // SecretString?? WebhookMessageFormat? MessageFormat ) : EntityBase(); diff --git a/src/ApiService/ApiService/onefuzzlib/WebhookOperations.cs b/src/ApiService/ApiService/onefuzzlib/WebhookOperations.cs index 3ed41398e4..1a829a2440 100644 --- a/src/ApiService/ApiService/onefuzzlib/WebhookOperations.cs +++ b/src/ApiService/ApiService/onefuzzlib/WebhookOperations.cs @@ -7,7 +7,7 @@ namespace Microsoft.OneFuzz.Service; -public interface IWebhookOperations { +public interface IWebhookOperations : IOrm { Async.Task SendEvent(EventMessage eventMessage); Async.Task GetByWebhookId(Guid webhookId); Async.Task Send(WebhookMessageLog messageLog); From c1c2b5b2fe85a8ffa83e3bbb73ff0255d19cefae Mon Sep 17 00:00:00 2001 From: Cheick Keita Date: Mon, 15 Aug 2022 14:45:20 -0700 Subject: [PATCH 2/5] webhook_logs and webhook_ping --- .../ApiService/Functions/WebhookLogs.cs | 49 +++++++++++++++++++ .../ApiService/Functions/WebhookPing.cs | 49 +++++++++++++++++++ .../onefuzzlib/WebhookOperations.cs | 10 ++++ 3 files changed, 108 insertions(+) create mode 100644 src/ApiService/ApiService/Functions/WebhookLogs.cs create mode 100644 src/ApiService/ApiService/Functions/WebhookPing.cs diff --git a/src/ApiService/ApiService/Functions/WebhookLogs.cs b/src/ApiService/ApiService/Functions/WebhookLogs.cs new file mode 100644 index 0000000000..89f1607aee --- /dev/null +++ b/src/ApiService/ApiService/Functions/WebhookLogs.cs @@ -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 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 Post(HttpRequestData req) { + var request = await RequestHandling.ParseRequest(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; + } +} diff --git a/src/ApiService/ApiService/Functions/WebhookPing.cs b/src/ApiService/ApiService/Functions/WebhookPing.cs new file mode 100644 index 0000000000..88bb689135 --- /dev/null +++ b/src/ApiService/ApiService/Functions/WebhookPing.cs @@ -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 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 Post(HttpRequestData req) { + var request = await RequestHandling.ParseRequest(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; + } +} diff --git a/src/ApiService/ApiService/onefuzzlib/WebhookOperations.cs b/src/ApiService/ApiService/onefuzzlib/WebhookOperations.cs index 1a829a2440..bc29cd2b40 100644 --- a/src/ApiService/ApiService/onefuzzlib/WebhookOperations.cs +++ b/src/ApiService/ApiService/onefuzzlib/WebhookOperations.cs @@ -2,6 +2,7 @@ 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; @@ -11,6 +12,7 @@ public interface IWebhookOperations : IOrm { Async.Task SendEvent(EventMessage eventMessage); Async.Task GetByWebhookId(Guid webhookId); Async.Task Send(WebhookMessageLog messageLog); + Task Ping(Webhook webhook); } public class WebhookOperations : Orm, IWebhookOperations { @@ -74,6 +76,14 @@ public async Async.Task Send(WebhookMessageLog messageLog) { return false; } + public async Task 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> BuildMessage(Guid webhookId, Guid eventId, EventType eventType, BaseEvent webhookEvent, String? secretToken, WebhookMessageFormat? messageFormat) { var entityConverter = new EntityConverter(); From 168d92c8239aaf1f453c2dcd96513b9c70d8275b Mon Sep 17 00:00:00 2001 From: Cheick Keita Date: Mon, 15 Aug 2022 17:13:17 -0700 Subject: [PATCH 3/5] fix saving uri to db --- src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs b/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs index 8caa9725d1..0bbf1a162d 100644 --- a/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs +++ b/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs @@ -187,7 +187,7 @@ public TableEntity ToTableEntity(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) From fef3048521a8f96c43c4fbe1bfbbf0f0929f04b8 Mon Sep 17 00:00:00 2001 From: Cheick Keita Date: Mon, 15 Aug 2022 17:42:43 -0700 Subject: [PATCH 4/5] fix tests --- src/ApiService/Tests/OrmTest.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ApiService/Tests/OrmTest.cs b/src/ApiService/Tests/OrmTest.cs index ca871138a0..089d64af3e 100644 --- a/src/ApiService/Tests/OrmTest.cs +++ b/src/ApiService/Tests/OrmTest.cs @@ -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(), @@ -71,7 +71,7 @@ public void TestBothDirections() { TheEnumValue = TestEnumValue.Two }, null, - new Uri(uriString), + uriString, null ); @@ -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(), @@ -121,7 +121,7 @@ public void TestConvertToTableEntity() { TheEnumValue = TestEnumValue.One }, null, - new Uri(uriString), + uriString, null ); var tableEntity = converter.ToTableEntity(entity1); @@ -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"); From 42901e9ba0d6e0cb542cb3fafa119a324f43737d Mon Sep 17 00:00:00 2001 From: Cheick Keita Date: Mon, 15 Aug 2022 18:41:16 -0700 Subject: [PATCH 5/5] fix deploy.py --- src/deployment/deploylib/registration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/deployment/deploylib/registration.py b/src/deployment/deploylib/registration.py index 8691c01726..9e5c35049d 100644 --- a/src/deployment/deploylib/registration.py +++ b/src/deployment/deploylib/registration.py @@ -367,10 +367,10 @@ def add_application_password_impl( password_request = { "passwordCredential": { "displayName": "%s" % password_name, - "startDateTime": "%s" % datetime.now(TZ_UTC).strftime("%Y-%m-%dT%H:%M.%fZ"), + "startDateTime": "%s" % datetime.now(TZ_UTC).strftime("%Y-%m-%dT%H:%M:%SZ"), "endDateTime": "%s" % (datetime.now(TZ_UTC) + timedelta(days=365)).strftime( - "%Y-%m-%dT%H:%M.%fZ" + "%Y-%m-%dT%H:%M:%SZ" ), } }