diff --git a/src/Clients/DevDad.SaaSAdmin.API/DevDad.SaaSAdmin.API.csproj b/src/Clients/DevDad.SaaSAdmin.API/DevDad.SaaSAdmin.API.csproj index 278a9d9..f190eb0 100644 --- a/src/Clients/DevDad.SaaSAdmin.API/DevDad.SaaSAdmin.API.csproj +++ b/src/Clients/DevDad.SaaSAdmin.API/DevDad.SaaSAdmin.API.csproj @@ -7,6 +7,7 @@ + @@ -14,6 +15,7 @@ + diff --git a/src/Clients/DevDad.SaaSAdmin.API/EndpointExtensions.cs b/src/Clients/DevDad.SaaSAdmin.API/EndpointExtensions.cs index be4b597..05c37bc 100644 --- a/src/Clients/DevDad.SaaSAdmin.API/EndpointExtensions.cs +++ b/src/Clients/DevDad.SaaSAdmin.API/EndpointExtensions.cs @@ -1,18 +1,20 @@ using System; -using System.Text.Json; using System.Threading.Tasks; -using DevDad.SaaSAdmin.AccountManager.Contracts; -using DevDad.SaaSAdmin.API.ApiServices; -using DevDad.SaaSAdmin.API.PublicModels; -using DevDad.SaaSAdmin.iFX; -using DevDad.SaaSAdmin.StoreManager.Contracts; + using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; + +using DevDad.SaaSAdmin.AccountManager.Contracts; +using DevDad.SaaSAdmin.API.ApiServices; +using DevDad.SaaSAdmin.API.PublicModels; +using DevDad.SaaSAdmin.StoreManager.Contracts; using ThatDeveloperDad.iFX.Serialization; +using Microsoft.AspNetCore.Authorization; +using DevDad.SaaSAdmin.iFX; namespace DevDad.SaaSAdmin.API; @@ -109,34 +111,44 @@ public static class EndpointExtensions await context.Response.WriteAsync("Hello There from AppEndpoints!"); }); - app.MapPost("/loadProfile", async Task (LoadProfileRequest requestData, HttpContext httpContext) => + app.MapPost("/loadProfile", + async Task (LoadProfileRequest requestData, HttpContext httpContext) => { IResult result = Results.NoContent(); try { IAccountManager? acctManager = componentRegistry.GetService(); - LoadAccountProfileRequest mgrRequest = new("LoadUserProfile") + LoadAccountProfileRequest mgrRequest = new("LoadOrCreateUserProfile") { UserId = requestData.UserEntraId }; - CustomerProfileResponse mgrResponse = await acctManager!.LoadCustomerProfileAsync(mgrRequest); + CustomerProfileResponse mgrResponse = await acctManager!.LoadOrCreateCustomerProfileAsync(mgrRequest); - if(mgrResponse.HasErrors) + if(mgrResponse.Payload == null) + { + result = Results.NotFound("No profile found for the provided Id."); + logger.LogInformation($"No profile found for UserEntraId {requestData.UserEntraId}"); + } + else if(mgrResponse.HasErrors) { result = Results.BadRequest(mgrResponse.ErrorReport); string errorReport = string.Join(Environment.NewLine, mgrResponse.ErrorReport); logger.LogError(errorReport); } - else if(mgrResponse.Payload == null) - { - result = Results.NotFound("No profile found for the provided Id."); - logger.LogInformation($"No profile found for UserEntraId {requestData.UserEntraId}"); - } else { - result = Results.Ok(mgrResponse.Payload); + LoadProfileResponse apiResponse = new() + { + UserId = mgrResponse.Payload.UserId, + DisplayName = mgrResponse.Payload.DisplayName, + SubscriptionSku = mgrResponse.Payload + .Subscription?.SKU + ?? SubscriptionIdentifiers.SKUS_TDMF_FREE + }; + + result = Results.Ok(apiResponse); logger.LogInformation($"Profile loaded successfully for UserEntraId {requestData.UserEntraId}"); } } @@ -148,7 +160,8 @@ public static class EndpointExtensions return result; }) - .Accepts("application/json"); + .Accepts("application/json") + .RequireAuthorization(ApiConstants.AuthorizationPolicies.AllowApiConsumersOnly); // This endpoint will be called from the Application when a user clicks on an // Upgrade to Paid Plan button. It will send the basic information to the diff --git a/src/Clients/DevDad.SaaSAdmin.API/Program.cs b/src/Clients/DevDad.SaaSAdmin.API/Program.cs index 9daeec8..b70dc66 100644 --- a/src/Clients/DevDad.SaaSAdmin.API/Program.cs +++ b/src/Clients/DevDad.SaaSAdmin.API/Program.cs @@ -1,13 +1,22 @@ using System; -using DevDad.SaaSAdmin.API.ApiServices; -using DotNetEnv; + using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Identity.Web; + +using DotNetEnv; + +using DevDad.SaaSAdmin.API.ApiServices; using ThatDeveloperDad.iFX; +using Microsoft.IdentityModel.Tokens; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Authorization; +using System.Linq; namespace DevDad.SaaSAdmin.API; @@ -20,33 +29,14 @@ public static void Main(string[] args) builder.Services.AddOpenApi(); var bootLogger = CreateBootLogger(); IConfiguration systemConfig = LoadSystemConfiguration(bootLogger); - builder = AddUtilityServices(systemConfig, bootLogger, builder); - - // Add services to the container. - builder.Services.AddAuthorization(options => - { - // Need to check the app Environment to determine if we're in Dev or Prod here. - var environment = builder.Environment; - - if (environment.IsDevelopment()) - { - options.AddPolicy(ApiConstants.AuthorizationPolicies.AllowApiConsumersOnly, - policy => policy.RequireAssertion(_ => true)); - } - else - { - options.AddPolicy(ApiConstants.AuthorizationPolicies.AllowApiConsumersOnly, - policy => policy.RequireAuthenticatedUser()); - } - }); - - // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi - + + builder = AddSecurityServices(systemConfig, bootLogger, builder); + builder = AddUtilityServices(systemConfig, bootLogger, builder); var app = builder.Build(); app.UseHttpsRedirection(); - + app.UseAuthentication(); app.UseAuthorization(); // All the services we'd registered in the Application's DI container are considered @@ -84,6 +74,68 @@ public static void Main(string[] args) app.Run(); } + static WebApplicationBuilder AddSecurityServices(IConfiguration configuration, + ILogger bootLog, + WebApplicationBuilder appBuilder) + { + bootLog.LogTrace("Configuring AuthN and AuthZ "); + + appBuilder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(options => { + appBuilder.Configuration.Bind("AzureAd", options); + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidAudience = configuration["AzureAd:Audience"], + ValidIssuer = $"{configuration["AzureAd:Instance"]}{configuration["AzureAd:TenantId"]}/v2.0" + }; + }, + options => { + appBuilder.Configuration.Bind("AzureAd", options); + }); + bootLog.LogTrace("Configured MS Identity & JWT Bearer options."); + + // Add services to the container. + appBuilder.Services.AddAuthorization(options => + { + + options.AddPolicy(ApiConstants.AuthorizationPolicies.AllowApiConsumersOnly, + policy => + policy.RequireAuthenticatedUser() + .RequireAssertion((AuthorizationHandlerContext context)=> + { + bootLog.LogTrace("Evaluationg AuthZ Policy."); + // Validate that the appId on the ConfidentialClientApp + // is in the list of approved apps. + string[] allowedClients = configuration + .GetSection("AzureAd:AllowedClients") + .Get() + ?? Array.Empty(); + + string appId = context.User.FindFirst("appid")?.Value??string.Empty; + + if(appId == string.Empty) + { bootLog.LogWarning("No AppId found in the JWT Token. Denying access."); + return false; + } + + bool appIdIsAllowed = allowedClients.Contains(appId); + if(!appIdIsAllowed) + { + bootLog.LogWarning($"AppId {appId} is not in the list of allowed clients. Denying access."); + } + else + { + bootLog.LogInformation($"AppId {appId} is in the list of allowed clients. Allowing access."); + } + return appIdIsAllowed; + })); + bootLog.LogTrace("Set policy to Restricted for prod env."); + }); + + return appBuilder; + } static WebApplicationBuilder AddUtilityServices(IConfiguration systemConfig, ILogger bootLog, diff --git a/src/Clients/DevDad.SaaSAdmin.API/PublicModels/LoadProfileResponse.cs b/src/Clients/DevDad.SaaSAdmin.API/PublicModels/LoadProfileResponse.cs new file mode 100644 index 0000000..ab6d1d2 --- /dev/null +++ b/src/Clients/DevDad.SaaSAdmin.API/PublicModels/LoadProfileResponse.cs @@ -0,0 +1,10 @@ +using System; + +namespace DevDad.SaaSAdmin.API.PublicModels; + +public class LoadProfileResponse +{ + public string UserId { get; set; } = string.Empty; + public string DisplayName {get;set;} = string.Empty; + public string SubscriptionSku { get; set; } = string.Empty; +} diff --git a/src/Clients/DevDad.SaaSAdmin.API/appsettings.json b/src/Clients/DevDad.SaaSAdmin.API/appsettings.json index edac7ac..2379193 100644 --- a/src/Clients/DevDad.SaaSAdmin.API/appsettings.json +++ b/src/Clients/DevDad.SaaSAdmin.API/appsettings.json @@ -6,6 +6,15 @@ } }, "AllowedHosts": "*", + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "6f9b52cc-54e3-4c18-a92d-35f0448d1e0d", + "ClientId": "58f44182-7cbf-4293-a78d-2e4528ca298f", + "Audience": "api://58f44182-7cbf-4293-a78d-2e4528ca298f", + "AllowedClients": [ + "63ff669d-b2fb-4367-8471-8605957304c6" + ] + }, "Architecture": { "GlobalBehaviors":[ diff --git a/src/Managers/DevDad.SaaSAdmin.AccountManager/CustomerAccountManager.cs b/src/Managers/DevDad.SaaSAdmin.AccountManager/CustomerAccountManager.cs index 37cbac4..f631b6e 100644 --- a/src/Managers/DevDad.SaaSAdmin.AccountManager/CustomerAccountManager.cs +++ b/src/Managers/DevDad.SaaSAdmin.AccountManager/CustomerAccountManager.cs @@ -112,6 +112,17 @@ public async Task LoadOrCreateCustomerProfileAsync(Load response.Payload = builderResponse.Payload; + // Get the user's current subscirption Sku. + // If there isn't one, default to the Free Sub SKU. + var userSubSku = builderResponse.Payload?.Subscription?.SKU + ?? SubscriptionIdentifiers.SKUS_TDMF_FREE; + + // Get the subscirption Tempalte for that SKU, and use it + // to ensure that the User is a member of the correct EntraID Groups + // for The DM's Familiar. + var skuTemplate = await _catalogAccess.GetCatalogItemAsync(userSubSku); + var reconciled = await TryReconcileUserMembership(requestData, response, requestData.UserId!, skuTemplate!); + if(response.Payload == null) { response.AddError(new ServiceError{ @@ -127,203 +138,217 @@ public async Task LoadOrCreateCustomerProfileAsync(Load public async Task ManageCustomerSubscriptionAsync(ManageSubscriptionRequest actionRequest) - { - ManageSubscriptionResponse thisResponse = new(actionRequest, null); - - // When a SubsriptionActionRequest arrives, we'll generally follow the same basic steps: - if(actionRequest == null) - { - ServiceError nullRequestPayload = new() - { - Message = "The SubscriptionActionRequest was null.", - Severity = ErrorSeverity.Error, - Site = $"{nameof(CustomerAccountManager)}.{nameof(ManageCustomerSubscriptionAsync)}", - ErrorKind = "NullRequestPayload" - }; - thisResponse.AddError(nullRequestPayload); - thisResponse.Payload = false; - return thisResponse; - } - - var requestErrors = TryGetChangeDetail(actionRequest, out SubscriptionActionDetail? actionDetail); - - // now, we can interrogate the requestErrors and run a null-check on the details. - if(requestErrors.Any()) - { - thisResponse.AddErrors(requestErrors); - thisResponse.Payload = false; - return thisResponse; - } - - if(actionDetail == null) - { - ServiceError nullRequestPayload = new() - { - Message = "The SubscriptionActionDetail is null.", - Severity = ErrorSeverity.Error, - Site = $"{nameof(CustomerAccountManager)}.{nameof(ManageCustomerSubscriptionAsync)}", - ErrorKind = "NullRequestPayload" - }; - thisResponse.AddError(nullRequestPayload); - thisResponse.Payload = false; - return thisResponse; - } - - // 1 & 2 are the same for every request. Do Those Here. - // 1: Load the Profile of the identified Customer. - BuildProfileRequest buildProfRequest = new(actionRequest, actionRequest.CustomerProfileId); - CustomerProfileResponse builderResponse = await AccountBuilder().LoadOrBuildCustomer(buildProfRequest); - - if(builderResponse.HasErrors) - { - thisResponse.AddErrors(builderResponse); - thisResponse.Payload = false; - return thisResponse; - } - - CustomerProfile? customerProfile = builderResponse.Payload; - - // If we can neither Load not Create a profile for the identified Customer, we need to bail now. - if(customerProfile == null) - { - ServiceError profileNotFound = new() - { - Message = $"The Customer Profile {actionRequest.CustomerProfileId} was not found.", - Severity = ErrorSeverity.Error, - Site = $"{nameof(CustomerAccountManager)}.{nameof(ManageCustomerSubscriptionAsync)}", - ErrorKind = "ProfileNotFound" - }; - thisResponse.AddError(profileNotFound); - thisResponse.Payload = false; - return thisResponse; - } - - string? userIdentityId = customerProfile.GetUserIdForVendor(_userIdentityAccess.IdentityVendor); - if(userIdentityId == null) - { - // Something Really, REALLY F***ed up happened to get to this edge case. - ServiceError noUserIdentityId = new() - { - Message = $"The Customer Profile {actionRequest.CustomerProfileId} has no Id in the Identity Service.", - Severity = ErrorSeverity.Error, - Site = $"{nameof(CustomerAccountManager)}.{nameof(ManageCustomerSubscriptionAsync)}", - ErrorKind = "NoUserIdentityId" - }; - thisResponse.AddError(noUserIdentityId); - thisResponse.Payload = false; - return thisResponse; - } - - // 2: Load the Catalog Item that gives us the details for the Subscription SKU - SubscriptionTemplateResource? skuTemplate = - await _catalogAccess.GetCatalogItemAsync(actionDetail.SubscriptionSku); + { + ManageSubscriptionResponse thisResponse = new(actionRequest, null); + + // When a SubsriptionActionRequest arrives, we'll generally follow the same basic steps: + if (actionRequest == null) + { + ServiceError nullRequestPayload = new() + { + Message = "The SubscriptionActionRequest was null.", + Severity = ErrorSeverity.Error, + Site = $"{nameof(CustomerAccountManager)}.{nameof(ManageCustomerSubscriptionAsync)}", + ErrorKind = "NullRequestPayload" + }; + thisResponse.AddError(nullRequestPayload); + thisResponse.Payload = false; + return thisResponse; + } + + var requestErrors = TryGetChangeDetail(actionRequest, out SubscriptionActionDetail? actionDetail); + + // now, we can interrogate the requestErrors and run a null-check on the details. + if (requestErrors.Any()) + { + thisResponse.AddErrors(requestErrors); + thisResponse.Payload = false; + return thisResponse; + } + + if (actionDetail == null) + { + ServiceError nullRequestPayload = new() + { + Message = "The SubscriptionActionDetail is null.", + Severity = ErrorSeverity.Error, + Site = $"{nameof(CustomerAccountManager)}.{nameof(ManageCustomerSubscriptionAsync)}", + ErrorKind = "NullRequestPayload" + }; + thisResponse.AddError(nullRequestPayload); + thisResponse.Payload = false; + return thisResponse; + } + + // 1 & 2 are the same for every request. Do Those Here. + // 1: Load the Profile of the identified Customer. + BuildProfileRequest buildProfRequest = new(actionRequest, actionRequest.CustomerProfileId); + CustomerProfileResponse builderResponse = await AccountBuilder().LoadOrBuildCustomer(buildProfRequest); + + if (builderResponse.HasErrors) + { + thisResponse.AddErrors(builderResponse); + thisResponse.Payload = false; + return thisResponse; + } + + CustomerProfile? customerProfile = builderResponse.Payload; + + // If we can neither Load not Create a profile for the identified Customer, we need to bail now. + if (customerProfile == null) + { + ServiceError profileNotFound = new() + { + Message = $"The Customer Profile {actionRequest.CustomerProfileId} was not found.", + Severity = ErrorSeverity.Error, + Site = $"{nameof(CustomerAccountManager)}.{nameof(ManageCustomerSubscriptionAsync)}", + ErrorKind = "ProfileNotFound" + }; + thisResponse.AddError(profileNotFound); + thisResponse.Payload = false; + return thisResponse; + } + + string? userIdentityId = customerProfile.GetUserIdForVendor(_userIdentityAccess.IdentityVendor); + if (userIdentityId == null) + { + // Something Really, REALLY F***ed up happened to get to this edge case. + ServiceError noUserIdentityId = new() + { + Message = $"The Customer Profile {actionRequest.CustomerProfileId} has no Id in the Identity Service.", + Severity = ErrorSeverity.Error, + Site = $"{nameof(CustomerAccountManager)}.{nameof(ManageCustomerSubscriptionAsync)}", + ErrorKind = "NoUserIdentityId" + }; + thisResponse.AddError(noUserIdentityId); + thisResponse.Payload = false; + return thisResponse; + } + + // 2: Load the Catalog Item that gives us the details for the Subscription SKU + SubscriptionTemplateResource? skuTemplate = + await _catalogAccess.GetCatalogItemAsync(actionDetail.SubscriptionSku); + + // If we can't load the Template for the provided subscription SKU, we cannot continue. + if (skuTemplate == null) + { + ServiceError skuNotFound = new() + { + Message = $"The Subscription SKU {actionDetail.SubscriptionSku} was not found in the Catalog.", + Severity = ErrorSeverity.Error, + Site = $"{nameof(CustomerAccountManager)}.{nameof(ManageCustomerSubscriptionAsync)}", + ErrorKind = "SkuNotFound" + }; + thisResponse.AddError(skuNotFound); + return thisResponse; + } + + // 3 & 4 are going to be different for each Activity. We'll use a Strategy Pattern for those. + // 3: Make sure the Activity is applicable to the current Subscription Status + // 4: Perform the Activity + ModifySubscriptionData changeSubData = new(customerProfile, actionDetail); + ModifySubscriptionRequest changeSubscriptionRequest = new(actionRequest, changeSubData); + var changeSubscriptionResponse = await AccountBuilder().PerformSubscriptionAction(changeSubscriptionRequest); + + if (changeSubscriptionResponse.HasErrors) + { + thisResponse.AddErrors(changeSubscriptionResponse); + thisResponse.Payload = false; + return thisResponse; + } + + if (changeSubscriptionResponse.HasWarnings) + { + thisResponse.AddErrors(changeSubscriptionResponse); + } + + if (changeSubscriptionResponse.Payload == null) + { + ServiceError noChangeResult = new() + { + Message = "The Change Subscription Activity did not return a result.", + Severity = ErrorSeverity.Error, + Site = $"{nameof(CustomerAccountManager)}.{nameof(ManageCustomerSubscriptionAsync)}", + ErrorKind = "NoChangeResult" + }; + thisResponse.AddError(noChangeResult); + thisResponse.Payload = false; + return thisResponse; + } + + customerProfile = changeSubscriptionResponse.Payload; + + // If the inbound Request has a VendorCustomerId, we need to ensure that that + // ID is stored with the Customer's local account profile. + customerProfile = UpdateVendorIds(customerProfile, actionDetail); + + // 5, 6, and 7 are the same for every request. Do Those Here. + // 5: Save the modified account back to local storage. + UserAccountResource accountResource = DomainObjectMapper + .MapEntities(customerProfile); + var saveResult = await _userAccountAccess.SaveUserAccountAsync(accountResource); + if (saveResult.Item2 != null) + { + thisResponse.AddError(new ServiceError + { + Message = saveResult.Item2.Message, + Severity = ErrorSeverity.Error, + Site = $"{nameof(CustomerAccountManager)}.{nameof(ManageCustomerSubscriptionAsync)}", + ErrorKind = "UserAccountSaveError" + }); + return thisResponse; + } + + // 6: Reconcile the Authorization Groups that the customer SHOULD have membership in at the Identity Service. + //This facility doesn't exist yet. We'll need to add it to the UserIdentityAccess service. + var reconciled = await TryReconcileUserMembership(actionRequest, thisResponse, userIdentityId, skuTemplate); + + // 7: Handle the result of all this stuff. + if (thisResponse.HasErrors) + { + thisResponse.Payload = false; + // Need a way to send an alert to a Human here. + } + else + { + thisResponse.Payload = true; + } + + return thisResponse; + } + + private async Task TryReconcileUserMembership( + OperationRequest actionRequest, + OperationResponse thisResponse, + string userIdentityId, + SubscriptionTemplateResource skuTemplate) + { + bool result = false; + + var reconcileData = new ReconcileMembershipsData + { + UserId = userIdentityId!, + ExpectedGroups = skuTemplate.ConfersMembershipIn + }; + + var reconcileGroupsRequest = new ReconcileMembershipsRequest + (actionRequest, + reconcileData); + + var reconcileResponse = await _userIdentityAccess.ReconcileUserMembershipsAsync(reconcileGroupsRequest); + if (reconcileResponse.HasErrors) + { + thisResponse.AddErrors(reconcileResponse); + + } - // If we can't load the Template for the provided subscription SKU, we cannot continue. - if(skuTemplate == null) - { - ServiceError skuNotFound = new() - { - Message = $"The Subscription SKU {actionDetail.SubscriptionSku} was not found in the Catalog.", - Severity = ErrorSeverity.Error, - Site = $"{nameof(CustomerAccountManager)}.{nameof(ManageCustomerSubscriptionAsync)}", - ErrorKind = "SkuNotFound" - }; - thisResponse.AddError(skuNotFound); - return thisResponse; - } - - // 3 & 4 are going to be different for each Activity. We'll use a Strategy Pattern for those. - // 3: Make sure the Activity is applicable to the current Subscription Status - // 4: Perform the Activity - ModifySubscriptionData changeSubData = new(customerProfile, actionDetail); - ModifySubscriptionRequest changeSubscriptionRequest = new(actionRequest, changeSubData); - var changeSubscriptionResponse = await AccountBuilder().PerformSubscriptionAction(changeSubscriptionRequest); - - if(changeSubscriptionResponse.HasErrors) - { - thisResponse.AddErrors(changeSubscriptionResponse); - thisResponse.Payload = false; - return thisResponse; - } - - if(changeSubscriptionResponse.HasWarnings) - { - thisResponse.AddErrors(changeSubscriptionResponse); - } - - if(changeSubscriptionResponse.Payload == null) - { - ServiceError noChangeResult = new() - { - Message = "The Change Subscription Activity did not return a result.", - Severity = ErrorSeverity.Error, - Site = $"{nameof(CustomerAccountManager)}.{nameof(ManageCustomerSubscriptionAsync)}", - ErrorKind = "NoChangeResult" - }; - thisResponse.AddError(noChangeResult); - thisResponse.Payload = false; - return thisResponse; - } - - customerProfile = changeSubscriptionResponse.Payload; - - // If the inbound Request has a VendorCustomerId, we need to ensure that that - // ID is stored with the Customer's local account profile. - customerProfile = UpdateVendorIds(customerProfile, actionDetail); - - // 5, 6, and 7 are the same for every request. Do Those Here. - // 5: Save the modified account back to local storage. - UserAccountResource accountResource = DomainObjectMapper - .MapEntities(customerProfile); - var saveResult = await _userAccountAccess.SaveUserAccountAsync(accountResource); - if(saveResult.Item2 != null) - { - thisResponse.AddError(new ServiceError{ - Message = saveResult.Item2.Message, - Severity = ErrorSeverity.Error, - Site = $"{nameof(CustomerAccountManager)}.{nameof(ManageCustomerSubscriptionAsync)}", - ErrorKind = "UserAccountSaveError" - }); - return thisResponse; - } - - // 6: Reconcile the Authorization Groups that the customer SHOULD have membership in at the Identity Service. - //This facility doesn't exist yet. We'll need to add it to the UserIdentityAccess service. - var reconcileData = new ReconcileMembershipsData - { - UserId = userIdentityId!, - ExpectedGroups = skuTemplate.ConfersMembershipIn - }; - - var reconcileGroupsRequest = new ReconcileMembershipsRequest - (actionRequest, - reconcileData); - - var reconcileResponse = await _userIdentityAccess.ReconcileUserMembershipsAsync(reconcileGroupsRequest); - if(reconcileResponse.HasErrors) - { - thisResponse.AddErrors(reconcileResponse); - thisResponse.Payload = false; - } - - string reconcileLog = $"Reconciled User Memberships for {userIdentityId} in {actionRequest.WorkloadName}: Added {reconcileResponse.MembershipsAdded}, Removed {reconcileResponse.MembershipsRemoved}"; - _logger?.LogInformation(reconcileLog); - - // 7: Handle the result of all this stuff. - if(thisResponse.HasErrors) - { - thisResponse.Payload = false; - // Need a way to send an alert to a Human here. - } - else - { - thisResponse.Payload = true; - } - - return thisResponse; - } + result = reconcileResponse.Successful; + string reconcileLog = $"Reconciled User Memberships for {userIdentityId} in {actionRequest.WorkloadName}: Added {reconcileResponse.MembershipsAdded}, Removed {reconcileResponse.MembershipsRemoved}"; + _logger?.LogInformation(reconcileLog); + return result; + } - public async Task StoreCustomerProfileAsync(SaveAccountProfileRequest request) + public async Task StoreCustomerProfileAsync(SaveAccountProfileRequest request) { CustomerProfileResponse response = new(request); diff --git a/src/ResourceAccess/DevDad.SaaSAdmin.UserAccountAccess.AzureTableProvider/DevDad.SaaSAdmin.UserAccountAccess.AzureTableProvider.csproj b/src/ResourceAccess/DevDad.SaaSAdmin.UserAccountAccess.AzureTableProvider/DevDad.SaaSAdmin.UserAccountAccess.AzureTableProvider.csproj index 7a89bac..34a79de 100644 --- a/src/ResourceAccess/DevDad.SaaSAdmin.UserAccountAccess.AzureTableProvider/DevDad.SaaSAdmin.UserAccountAccess.AzureTableProvider.csproj +++ b/src/ResourceAccess/DevDad.SaaSAdmin.UserAccountAccess.AzureTableProvider/DevDad.SaaSAdmin.UserAccountAccess.AzureTableProvider.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Tests/TestConsole/Program.cs b/src/Tests/TestConsole/Program.cs index 187f9e8..37f7df3 100644 --- a/src/Tests/TestConsole/Program.cs +++ b/src/Tests/TestConsole/Program.cs @@ -1,16 +1,13 @@ using System.Net.Http.Json; -using DevDad.SaaSAdmin.AccountManager.Contracts; -using DevDad.SaaSAdmin.iFX; -using DevDad.SaaSAdmin.StoreAccess.Abstractions; -using DevDad.SaaSAdmin.StoreAccess.LsApi; + using DotNetEnv; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using TestConsole.OtherTests; -using ThatDeveloperDad.iFX; -using ThatDeveloperDad.iFX.CollectionUtilities; -using ThatDeveloperDad.iFX.DomainUtilities; +using DevDad.SaaSAdmin.iFX; +using DevDad.SaaSAdmin.StoreAccess.Abstractions; +using DevDad.SaaSAdmin.StoreAccess.LsApi; using ThatDeveloperDad.iFX.Serialization; namespace TestConsole @@ -24,7 +21,7 @@ static void Main(string[] args) //TestJsonDrill(); //TestRequestConstruction(bootLogger); - TestHostedApi(bootLogger); + /* IConfiguration systemConfig = LoadSystemConfiguration(bootLogger); IServiceProvider globalUtilities = BuildUtilityProvider(systemConfig, bootLogger); @@ -111,24 +108,7 @@ static void Main(string[] args) bootLogger.LogInformation("Nothing more to do. Imma take a nap right here."); } - static void TestHostedApi(ILogger logger) - { - string url = "https://tdmf-admin-api-f4a5caaydzgncqf4.eastus2-01.azurewebsites.net/loadProfile"; - string userId = "a996d415-159c-47fa-ae5a-4b6db581ebb1"; - var request = new - { - UserEntraId = userId - }; - - var apiCLient = new HttpClient(); - - var response = apiCLient.PostAsJsonAsync(url, request).Result; - - var responseContent = response.Content.ReadAsStringAsync().Result; - - Console.WriteLine(response.StatusCode); - Console.WriteLine(responseContent); - } + static void TestWebhookProcessor(ILogger logger) { diff --git a/src/Tests/TestConsole/appsettings.json b/src/Tests/TestConsole/appsettings.json index 993cbf1..0e2e371 100644 --- a/src/Tests/TestConsole/appsettings.json +++ b/src/Tests/TestConsole/appsettings.json @@ -6,6 +6,7 @@ "Microsoft.Hosting.Lifetime":"Information" } }, + "applicationIdUri":"api://58f44182-7cbf-4293-a78d-2e4528ca298f", "Architecture": { "GlobalBehaviors":[