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
2 changes: 1 addition & 1 deletion .github/workflows/main_tdmf-admin-api.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions

name: Build and deploy ASP.Net Core API app to Azure Web App - tdmf-admin-api
name: Build and deploy Azure Web App - tdmf-admin-api

on:
workflow_dispatch:
Expand Down
24 changes: 12 additions & 12 deletions .github/workflows/main_tdmf-public-hooks.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action
# More GitHub Actions for Azure: https://github.com/Azure/actions

name: Build and deploy dotnet core project to Azure Function App - tdmf-public-hooks
name: Build and deploy Azure Function App - tdmf-public-hooks

on:
push:
Expand All @@ -10,15 +10,15 @@ on:
workflow_dispatch:

env:
AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root
AZURE_FUNCTIONAPP_PACKAGE_PATH: './src/Clients/DevDad.SaaSAdmin.Functions' # set this to the path to your web app project, defaults to the repository root
DOTNET_VERSION: '9.0.x' # set this to the dotnet version to use

jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
id-token: write #This is required for requesting the JWT
contents: read #This is required for actions/checkout
permissions:
id-token: write #This is required for requesting the JWT
contents: read #This is required for actions/checkout

steps:
- name: 'Checkout GitHub Action'
Expand All @@ -35,13 +35,13 @@ jobs:
pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}'
dotnet build --configuration Release --output ./output
popd

- name: Login to Azure
uses: azure/login@v2
with:
client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_31C4C1848B96433D81A355850C97B318 }}
tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_63947D7FB2AF42F1A5FBB716C25F3DF6 }}
subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_9F51F8E8636A4A83A16FA2AF6BD8355B }}
- name: Login to Azure
uses: azure/login@v2
with:
client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_31C4C1848B96433D81A355850C97B318 }}
tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_63947D7FB2AF42F1A5FBB716C25F3DF6 }}
subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_9F51F8E8636A4A83A16FA2AF6BD8355B }}

- name: 'Run Azure Functions Action'
uses: Azure/functions-action@v1
Expand Down
40 changes: 20 additions & 20 deletions src/Clients/DevDad.SaaSAdmin.API/EndpointExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,7 @@ public static class EndpointExtensions
IResult? operationResult = null;
IAccountManager acctMgr = componentRegistry.GetRequiredService<IAccountManager>();

// Disallow "test" events in non-development environments.
// Might want to reconsider this for testing in Azure though.
if(JsonUtilities.GetValueAtPath(lsEvent.EventJson, "$.data.test_mode") == "true"
&& app.Environment.IsDevelopment() == false)
{
logger?.LogWarning("Received a test event in a non-development environment. Ignoring.");
return Results.BadRequest<string>("TestEvents are not allowed against non-development mode APIs.");
}


operationResult = await EndpointLogic
.ProcessStoreEvent(
Expand All @@ -79,13 +72,14 @@ public static class EndpointExtensions

return operationResult;
})
.WithName("ProcessStoreEvent");
.WithName("ProcessStoreEvent")
.RequireAuthorization(ApiConstants.AuthorizationPolicies.AllowApiConsumersOnly);

webHookRoutes.MapPost("/processNewEntraSignup", async (HttpContext context) =>
/* webHookRoutes.MapPost("/processNewEntraSignup", async (HttpContext context) =>
{
await context.Response.WriteAsync("Processed New Entra Signup!");
})
.WithName("ProcessNewEntraSignup");
.WithName("ProcessNewEntraSignup"); */

return app;
}
Expand Down Expand Up @@ -155,7 +149,9 @@ async Task<IResult> (LoadProfileRequest requestData, HttpContext httpContext) =>
catch(Exception ex)
{
logger?.LogError(ex, "An error occurred while processing the LoadProfile request.");
result = Results.InternalServerError<string?>("An error occurred while processing your request.");
result = Results.Problem(
detail: "An error occurred while processing your request.",
statusCode: StatusCodes.Status500InternalServerError);
}

return result;
Expand All @@ -180,7 +176,9 @@ async Task<IResult> (LoadProfileRequest requestData, HttpContext httpContext) =>
if(storeMgr==null)
{
logger.LogCritical("The StoreManager could not be instantiated to perform the operation.");
return Results.InternalServerError<string?>("An error occurred while processing your request.");
return Results.Problem(
detail: "An error occurred while processing your request.",
statusCode: StatusCodes.Status500InternalServerError);
}
// convert the inbound requestData into the type expected by the
// StoreManager.
Expand All @@ -195,7 +193,9 @@ async Task<IResult> (LoadProfileRequest requestData, HttpContext httpContext) =>

if(mgrResponse == null)
{
result = Results.InternalServerError<string?>("An error occurred while processing your request.");
result = Results.Problem(
detail: "An error occurred while processing your request.",
statusCode: StatusCodes.Status500InternalServerError);
logger.LogError("The StoreManager could not be instantiated to perform the operation.");
}
else if(mgrResponse.HasErrors)
Expand All @@ -213,15 +213,15 @@ async Task<IResult> (LoadProfileRequest requestData, HttpContext httpContext) =>
catch(Exception ex)
{
logger.LogError(ex, "An error occurred while processing the StartUpgrade request.");
result = Results.InternalServerError<string?>("An error occurred while processing your request.");
result = Results.Problem(
detail:"An error occurred while processing your request.",
statusCode: StatusCodes.Status500InternalServerError);
}

return result;
});
//TODO: Sort out what we need to do to require Authorization on this call.
// Once we've done that, we need to make sure all callers are set up correctly.
// Probably require membership in a specific Entra Group.
// Add myself as a backdoor.
})
.RequireAuthorization(ApiConstants.AuthorizationPolicies.AllowApiConsumersOnly);


return app;
}
Expand Down
3 changes: 2 additions & 1 deletion src/Clients/DevDad.SaaSAdmin.API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"ClientId": "58f44182-7cbf-4293-a78d-2e4528ca298f",
"Audience": "api://58f44182-7cbf-4293-a78d-2e4528ca298f",
"AllowedClients": [
"63ff669d-b2fb-4367-8471-8605957304c6"
"63ff669d-b2fb-4367-8471-8605957304c6",
"57e294f8-0711-4dd1-b6f0-1870f8459233"
]
},
"Architecture":
Expand Down
1 change: 1 addition & 0 deletions src/Clients/DevDad.SaaSAdmin.Functions/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
local.settings.json

# User-specific files
*.env
*.suo
*.user
*.userosscache
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
<TargetFramework>net9.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Azure.Identity" Version="1.13.2" />
<!-- Application Insights isn't enabled by default. See https://aka.ms/AAt8mw4. -->
<!-- <PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" /> -->
<!-- <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.0.0" /> -->
Expand All @@ -16,6 +17,9 @@
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.0.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../iFX/ThatDeveloperDad.iFX/ThatDeveloperDad.iFX.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
Expand All @@ -31,6 +35,10 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
<None Update=".env">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Using Include="System.Threading.ExecutionContext" Alias="ExecutionContext" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Identity;
using Microsoft.Extensions.Logging;

namespace DevDad.SaaSAdmin.Functions.LocalServices;

public class AdminApiOptions
{
public string TenantId { get; set; } = string.Empty;
public string ClientId { get; set; } = string.Empty;
public string ClientSecret { get; set; } = string.Empty;

public string RequiredScope { get; set; } = string.Empty;

public string ApiBaseUrl { get; set; } = string.Empty;

internal static async Task<string> GetTokenAsync(
AdminApiOptions options,
ILogger? logger = null)
{
string authToken = string.Empty;
try
{

var credential = new ClientSecretCredential(
options.TenantId,
options.ClientId,
options.ClientSecret);

var token = await credential.GetTokenAsync(
new TokenRequestContext(new[] { options.RequiredScope }));
authToken = token.Token;
}
catch(Exception ex){
logger?.LogError(ex, "Failed to get token");
}

return authToken;
}
}
46 changes: 36 additions & 10 deletions src/Clients/DevDad.SaaSAdmin.Functions/OnStoreAction.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using DevDad.SaaSAdmin.Functions.ApiModels;
using DevDad.SaaSAdmin.Functions.LocalServices;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using ThatDeveloperDad.iFX.Serialization;

namespace DevDad.SaaSAdmin.Functions
{
Expand All @@ -16,14 +21,29 @@ public class OnStoreAction
private readonly ILogger<OnStoreAction> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _config;
private readonly AdminApiOptions _apiOptions;

private readonly string _signingSecret;

public OnStoreAction(ILogger<OnStoreAction> logger,
IHttpClientFactory httpClientFactory,
IConfiguration config)
IConfiguration config,
AdminApiOptions apiOptions)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
_config = config;
_apiOptions = apiOptions;

string? lsSecret = _config["LemonSqueezySigningSecret"];
if(string.IsNullOrWhiteSpace(lsSecret))
{
throw new InvalidOperationException("The LemonSqueezy signing secret is not configured.");
}
else
{
_signingSecret = lsSecret;
}
}

[Function("OnStoreAction")]
Expand All @@ -40,6 +60,15 @@ public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "
rawBody = await reader.ReadToEndAsync();
}

// Disallow "test" events when system is not in test mode.
if(JsonUtilities
.GetValueAtPath(rawBody, "$.data.test_mode") == "true"
&& _config["LemonSqueezyIsTestMode"] != "true")
{
_logger?.LogWarning("Received a test event in a non-development environment. Ignoring.");
return new BadRequestResult();
}

//Validate Request Signature. If not valid, reject & return 401.
bool signerCorrect = ValidateRequestSignature(req.Headers["X-Signature"], rawBody);

Expand All @@ -50,9 +79,10 @@ public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "
EventJson = rawBody,
RequestId = req.HttpContext.TraceIdentifier
};

string apiBearer = await AdminApiOptions.GetTokenAsync(_apiOptions, _logger);

HttpClient apiClient = CreateApiClient();
apiClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiBearer}");
string url = apiClient.BaseAddress + apiEndpointPath;

var apiResponse = await apiClient.PostAsJsonAsync(url, eventData);
Expand All @@ -79,15 +109,14 @@ public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "
_logger?.LogError(e, "An error occurred while processing the request.");
result = new StatusCodeResult(StatusCodes.Status500InternalServerError);
}


_logger?.LogInformation("C# HTTP trigger function processed a request for OnStoreAction.");
return result;
}

private HttpClient CreateApiClient()
{
string? baseUrl = _config["adminApi:baseUrl"];
string? baseUrl = _apiOptions.ApiBaseUrl;
if(string.IsNullOrWhiteSpace(baseUrl))
{
throw new InvalidOperationException("The base URL for the Admin API is not configured.");
Expand All @@ -109,11 +138,8 @@ private bool ValidateRequestSignature(string? x_sig, string body)

bool signerCorrect = false;

// TODO: stuff the real value in .env / environment vars.
// this is a test-only value and won't be used in production.
string signingSecret = "GGEDpCdG,VVFq!YoTsL+)_+L|kT?N[";
// Need to has the signing secret w/ HMAC SHA256
using(var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(signingSecret)))
// Need to hash the signing secret w/ HMAC SHA256
using(var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_signingSecret)))
{
var digest = hmac.ComputeHash(Encoding.UTF8.GetBytes(body));
var digestHex = BitConverter.ToString(digest).Replace("-", "").ToLower();
Expand Down
13 changes: 8 additions & 5 deletions src/Clients/DevDad.SaaSAdmin.Functions/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using DevDad.SaaSAdmin.Functions.LocalServices;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = FunctionsApplication.CreateBuilder(args);

Expand All @@ -10,13 +12,14 @@
.AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();

var apiOptions = new AdminApiOptions();
builder.Configuration.GetSection("AdminApi").Bind(apiOptions);

builder.Services.AddSingleton(apiOptions);
builder.ConfigureFunctionsWebApplication();
builder.Services.AddLogging();
builder.Services.AddHttpClient();
builder.Logging.AddConsole();

// Application Insights isn't enabled by default. See https://aka.ms/AAt8mw4.
// builder.Services
// .AddApplicationInsightsTelemetryWorkerService()
// .ConfigureFunctionsApplicationInsights();
builder.Services.AddHttpClient();

builder.Build().Run();
6 changes: 1 addition & 5 deletions src/Clients/DevDad.SaaSAdmin.Functions/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,5 @@
}
},
"AllowedHosts": "*",
"WebhookSigningKey": "hahaNo.",
"adminApi":{
"baseUrl": "http://localhost:5083",
"apiKey": "lolNope"
}
"WebhookSigningKey": "hahaNo."
}
Loading
Loading