diff --git a/ready/AzNamingTool/App.razor b/ready/AzNamingTool/App.razor index b2d3de4e..344f424f 100644 --- a/ready/AzNamingTool/App.razor +++ b/ready/AzNamingTool/App.razor @@ -1,4 +1,7 @@ - +@using AzureNamingTool.Models; + + + @@ -13,3 +16,8 @@ + +@code { + [Parameter] + public IdentityProviderDetails? identityProviderDetails { get; set; } +} diff --git a/ready/AzNamingTool/AzureNamingTool.csproj b/ready/AzNamingTool/AzureNamingTool.csproj index 69f30fe9..dfa9c30b 100644 --- a/ready/AzNamingTool/AzureNamingTool.csproj +++ b/ready/AzNamingTool/AzureNamingTool.csproj @@ -1,7 +1,7 @@  - 2.7.0 + 2.8.0 net6.0 enable enable @@ -19,13 +19,6 @@ 1591;1701;1702 - - - - - - - @@ -73,6 +66,9 @@ Always + + Never + @@ -81,4 +77,8 @@ + + + + diff --git a/ready/AzNamingTool/Controllers/ResourceNamingRequestsController.cs b/ready/AzNamingTool/Controllers/ResourceNamingRequestsController.cs index 29470797..e391ff72 100644 --- a/ready/AzNamingTool/Controllers/ResourceNamingRequestsController.cs +++ b/ready/AzNamingTool/Controllers/ResourceNamingRequestsController.cs @@ -64,6 +64,7 @@ public async Task RequestName([FromBody] ResourceNameRequest requ { try { + request.CreatedBy = "API"; ResourceNameResponse resourceNameRequestResponse = await ResourceNamingRequestService.RequestName(request); if (resourceNameRequestResponse.Success) diff --git a/ready/AzNamingTool/Helpers/CacheHelper.cs b/ready/AzNamingTool/Helpers/CacheHelper.cs index e4885de1..4f866838 100644 --- a/ready/AzNamingTool/Helpers/CacheHelper.cs +++ b/ready/AzNamingTool/Helpers/CacheHelper.cs @@ -80,5 +80,22 @@ public static string GetAllCacheData() } return data.ToString(); } + + public static void ClearAllCache() + { + try + { + ObjectCache memoryCache = MemoryCache.Default; + List cacheKeys = memoryCache.Select(kvp => kvp.Key).ToList(); + foreach (string cacheKey in cacheKeys) + { + memoryCache.Remove(cacheKey); + } + } + catch (Exception ex) + { + AdminLogService.PostItem(new AdminLogMessage() { Title = "ERROR", Message = ex.Message }); + } + } } } diff --git a/ready/AzNamingTool/Helpers/ConfigurationHelper.cs b/ready/AzNamingTool/Helpers/ConfigurationHelper.cs index 038a6159..4a17d8be 100644 --- a/ready/AzNamingTool/Helpers/ConfigurationHelper.cs +++ b/ready/AzNamingTool/Helpers/ConfigurationHelper.cs @@ -13,6 +13,7 @@ using System.Net.Http.Headers; using System; using Blazored.Toast.Services; +using Microsoft.AspNetCore.Mvc; namespace AzureNamingTool.Helpers { @@ -39,15 +40,29 @@ public static string GetAppSetting(string key, bool decrypt = false) { var config = GetConfigurationData(); + // Check if the app setting is already set if (config.GetType().GetProperty(key) != null) { value = config.GetType().GetProperty(key).GetValue(config, null).ToString(); - if ((decrypt) && (value != "")) + // Verify the value is encrypted, and should be decrypted + if ((decrypt) && (value != "") && (GeneralHelper.IsBase64Encoded(value))) { value = GeneralHelper.DecryptString(value, config.SALTKey); } + // Set the result to cache + CacheHelper.SetCacheObject(key, value); + } + else + { + // Create a new configuration object and get the default for the property + SiteConfiguration newconfig = new(); + value = newconfig.GetType().GetProperty(key).GetValue(newconfig, null).ToString(); + + // Set the result to the app settings + SetAppSetting(key, value, decrypt); + // Set the result to cache CacheHelper.SetCacheObject(key, value); } @@ -87,7 +102,7 @@ public static void SetAppSetting(string key, string value, bool encrypt = false) } } - public static void VerifyConfiguration() + public static async void VerifyConfiguration(StateContainer state) { try { @@ -110,6 +125,13 @@ public static void VerifyConfiguration() // Migrate the data FileSystemHelper.MigrateDataToFile("adminlog.json", "settings/", "adminlogmessages.json", "settings/", true); } + + // Sync cnfiguration data + if (!state.ConfigurationDataSynced) + { + await SyncConfigurationData("ResourceComponent"); + state.SetConfigurationDataSynced(true); + } } catch (Exception ex) { @@ -187,11 +209,18 @@ public static bool VerifyConnectivity() byte[] buffer = new byte[32]; int timeout = 1000; PingOptions pingOptions = new(); - PingReply reply = ping.Send(host, timeout, buffer, pingOptions); - if (reply.Status == IPStatus.Success) + try { - pingsuccessful = true; - result = true; + PingReply reply = ping.Send(host, timeout, buffer, pingOptions); + if (reply.Status == IPStatus.Success) + { + pingsuccessful = true; + result = true; + } + } + catch (Exception) + { + // Catch this exception but continue to try a web request instead } // If ping is not successful, attempt to download a file @@ -201,16 +230,14 @@ public static bool VerifyConnectivity() var request = (HttpWebRequest)WebRequest.Create("https://github.com/aznamingtool/AzureNamingTool/blob/main/connectiontest.png"); request.KeepAlive = false; request.Timeout = 1500; - using (var response = (HttpWebResponse)request.GetResponse()) + using var response = (HttpWebResponse)request.GetResponse(); + if (response.StatusCode == HttpStatusCode.OK) { - if (response.StatusCode == HttpStatusCode.OK) - { - result = true; - } - else - { - AdminLogService.PostItem(new AdminLogMessage() { Title = "ERROR", Message = "Connectivity Check Failed:" + response.StatusDescription }); - } + result = true; + } + else + { + AdminLogService.PostItem(new AdminLogMessage() { Title = "ERROR", Message = "Connectivity Check Failed:" + response.StatusDescription }); } } } @@ -226,7 +253,7 @@ public static bool VerifyConnectivity() } catch (Exception ex) { - AdminLogService.PostItem(new AdminLogMessage() { Title = "ERROR", Message = ex.Message }); + AdminLogService.PostItem(new AdminLogMessage() { Title = "ERROR", Message = "There was a problem verifying connectivty. Error: " + ex.Message }); } // Set the result to cache @@ -258,6 +285,7 @@ public async static Task> GetList() nameof(CustomComponent) => await FileSystemHelper.ReadFile("customcomponents.json"), nameof(AdminLogMessage) => await FileSystemHelper.ReadFile("adminlogmessages.json"), nameof(GeneratedName) => await FileSystemHelper.ReadFile("generatednames.json"), + nameof(AdminUser) => await FileSystemHelper.ReadFile("adminusers.json"), _ => "[]", }; CacheHelper.SetCacheObject(typeof(T).Name, data); @@ -323,6 +351,9 @@ public async static Task WriteList(List items) case nameof(GeneratedName): await FileSystemHelper.WriteConfiguation(items, "generatednames.json"); break; + case nameof(AdminUser): + await FileSystemHelper.WriteConfiguation(items, "adminusers.json"); + break; default: break; } @@ -342,6 +373,7 @@ public async static Task WriteList(List items) nameof(CustomComponent) => await FileSystemHelper.ReadFile("customcomponents.json"), nameof(AdminLogMessage) => await FileSystemHelper.ReadFile("adminlogmessages.json"), nameof(GeneratedName) => await FileSystemHelper.ReadFile("generatednames.json"), + nameof(AdminUser) => await FileSystemHelper.ReadFile("adminusers.json"), _ => "[]", }; @@ -356,6 +388,13 @@ public async static Task WriteList(List items) public static async void UpdateSettings(SiteConfiguration config) { + // Clear the cache + ObjectCache memoryCache = MemoryCache.Default; + List cacheKeys = memoryCache.Select(kvp => kvp.Key).ToList(); + foreach (string cacheKey in cacheKeys) + { + memoryCache.Remove(cacheKey); + } var jsonWriteOptions = new JsonSerializerOptions() { WriteIndented = true @@ -519,7 +558,7 @@ public static void ResetState(StateContainer state) state.SetVerified(false); state.SetAdmin(false); state.SetPassword(false); - state.SetAppTheme("bg-default text-black"); + state.SetAppTheme("bg-default text-dark"); } public static async Task GetToolVersion() @@ -702,5 +741,92 @@ public static async Task GetProgramSetting(string programSetting) } return result; } + + /// + /// This function is used to sync default configuration data with the user's local version + /// + /// string - Type of configuration data to sync + public static async Task SyncConfigurationData(string type) + { + try + { + bool update = false; + + switch (type) + { + case "ResourceComponent": + // Get all the existing components + List currentComponents = new(); + ServiceResponse serviceResponse = new(); + serviceResponse = await ResourceComponentService.GetItems(true); + if (serviceResponse.Success) + { + currentComponents = serviceResponse.ResponseObject; + // Get the default component data + List defaultComponents = new(); + string data = await FileSystemHelper.ReadFile("resourcecomponents.json", "repository/"); + if (!String.IsNullOrEmpty(data)) + { + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true + }; + + if (!String.IsNullOrEmpty(data)) + { + defaultComponents = JsonSerializer.Deserialize>(data, options); + } + + // Loop over the existing components to verify the data is complete + foreach (ResourceComponent currentComponent in currentComponents) + { + // Create a new component for any updates + ResourceComponent newComponent = currentComponent; + // Get the matching default component for the current component + ResourceComponent defaultcomponent = defaultComponents.Find(x => x.Name == currentComponent.Name); + // Check the data to see if it's been configured + if (String.IsNullOrEmpty(currentComponent.MinLength)) + { + if (defaultcomponent != null) + { + newComponent.MinLength = defaultcomponent.MinLength; + } + else + { + newComponent.MinLength = "1"; + } + update = true; + } + + // Check the data to see if it's been configured + if (String.IsNullOrEmpty(currentComponent.MaxLength)) + { + if (defaultcomponent != null) + { + newComponent.MaxLength = defaultcomponent.MaxLength; + } + else + { + newComponent.MaxLength = "10"; + } + update = true; + } + if (update) + { + await ResourceComponentService.PostItem(newComponent); + } + } + } + } + break; + } + } + catch (Exception ex) + { + await AdminLogService.PostItem(new AdminLogMessage() { Title = "ERROR", Message = ex.Message }); + } + + } } } diff --git a/ready/AzNamingTool/Helpers/FileSystemHelper.cs b/ready/AzNamingTool/Helpers/FileSystemHelper.cs index 4d362df7..f949cd20 100644 --- a/ready/AzNamingTool/Helpers/FileSystemHelper.cs +++ b/ready/AzNamingTool/Helpers/FileSystemHelper.cs @@ -30,13 +30,11 @@ public static async Task WriteFile(string fileName, string content, string folde { try { - using (FileStream fstr = File.Open(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, folderName + fileName), FileMode.Truncate, FileAccess.Write)) - { - StreamWriter sw = new StreamWriter(fstr); - sw.Write(content); - sw.Flush(); - sw.Dispose(); - } + using FileStream fstr = File.Open(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, folderName + fileName), FileMode.Truncate, FileAccess.Write); + StreamWriter sw = new(fstr); + sw.Write(content); + sw.Flush(); + sw.Dispose(); return; } catch (Exception) diff --git a/ready/AzNamingTool/Helpers/GeneralHelper.cs b/ready/AzNamingTool/Helpers/GeneralHelper.cs index ee456ca8..6fc5233f 100644 --- a/ready/AzNamingTool/Helpers/GeneralHelper.cs +++ b/ready/AzNamingTool/Helpers/GeneralHelper.cs @@ -59,6 +59,22 @@ public static string DecryptString(string cipherText, string keyString) return streamReader.ReadToEnd(); } + public static bool IsBase64Encoded(string value) + { + bool base64encoded = false; + try + { + byte[] byteArray = Convert.FromBase64String(value); + base64encoded = true; + } + catch (FormatException) + { + // The string is not base 64. Dismiss the error and return false + } + return base64encoded; + } + + public static async Task DownloadString(string url) { string data; diff --git a/ready/AzNamingTool/Helpers/IdentityHelper.cs b/ready/AzNamingTool/Helpers/IdentityHelper.cs new file mode 100644 index 00000000..39f663f8 --- /dev/null +++ b/ready/AzNamingTool/Helpers/IdentityHelper.cs @@ -0,0 +1,53 @@ +using AzureNamingTool.Models; +using AzureNamingTool.Services; +using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; +using System; + +namespace AzureNamingTool.Helpers +{ + public class IdentityHelper + { + public static async Task IsAdminUser(StateContainer state, ProtectedSessionStorage session, string name) + { + bool result = false; + try + { + // Check if the username is in the list of Admin Users + ServiceResponse serviceResponse = await AdminUserService.GetItems(); + if (serviceResponse.Success) + { + List adminusers = serviceResponse.ResponseObject; + if (adminusers.Exists(x => x.Name.ToLower() == name.ToLower())) + { + state.SetAdmin(true); + await session.SetAsync("admin", true); + result = true; + } + } + } + catch (Exception ex) + { + AdminLogService.PostItem(new AdminLogMessage() { Title = "ERROR", Message = ex.Message }); + } + return result; + } + + public static async Task GetCurrentUser(ProtectedSessionStorage session) + { + string currentuser = "System"; + try + { + var currentuservalue = await session.GetAsync("currentuser"); + if (!String.IsNullOrEmpty(currentuservalue.Value)) + { + currentuser = currentuservalue.Value; + } + } + catch (Exception ex) + { + AdminLogService.PostItem(new AdminLogMessage() { Title = "ERROR", Message = ex.Message }); + } + return currentuser; + } + } +} diff --git a/ready/AzNamingTool/Helpers/ServicesHelper.cs b/ready/AzNamingTool/Helpers/ServicesHelper.cs index 8e5d6250..2ccc1588 100644 --- a/ready/AzNamingTool/Helpers/ServicesHelper.cs +++ b/ready/AzNamingTool/Helpers/ServicesHelper.cs @@ -35,6 +35,8 @@ public static async Task LoadServicesData(ServicesData servicesDat servicesData.GeneratedNames = (List)serviceReponse.ResponseObject; serviceReponse = await AdminLogService.GetItems(); servicesData.AdminLogMessages = (List)serviceReponse.ResponseObject; + serviceReponse = await AdminUserService.GetItems(); + servicesData.AdminUsers = (List)serviceReponse.ResponseObject; return servicesData; } catch(Exception ex) diff --git a/ready/AzNamingTool/Helpers/ValidationHelper.cs b/ready/AzNamingTool/Helpers/ValidationHelper.cs index 2a7d0a4a..77cfdee6 100644 --- a/ready/AzNamingTool/Helpers/ValidationHelper.cs +++ b/ready/AzNamingTool/Helpers/ValidationHelper.cs @@ -18,62 +18,45 @@ public static bool ValidatePassword(string text) return isValidated; } - public static bool ValidateShortName(string value, string type) + public static async Task ValidateShortName(string type, string value, string parentcomponent = null) { bool valid = false; - - switch (type) + try { - case "ResourceEnvironment": - if (value.Length < 6) - { - valid = true; - } - break; - case "ResourceLocation": - if (value.Length < 11) - { - valid = true; - } - break; - case "ResourceOrg": - if (value.Length < 6) - { - valid = true; - } - break; - case "ResourceProjAppSvc": - if (value.Length < 4) - { - valid = true; - } - break; - case "ResourceType": - if (value.Length < 11) - { - valid = true; - } - break; - case "ResourceUnitDept": - if (value.Length < 4) + ResourceComponent resourceComponent = new(); + List resourceComponents = new(); + ServiceResponse serviceResponse; + + // Get the current components + serviceResponse = await ResourceComponentService.GetItems(true); + if (serviceResponse.Success) + { + resourceComponents = (List)serviceResponse.ResponseObject; + + // Check if it's a custom component + if (type == "CustomComponent") { - valid = true; + resourceComponent = resourceComponents.Find(x => GeneralHelper.NormalizeName(x.Name, true) == GeneralHelper.NormalizeName(parentcomponent, true)); } - break; - case "ResourceFunction": - if (value.Length < 11) + else { - valid = true; + resourceComponent = resourceComponents.Find(x => x.Name == type); } - break; - case "CustomComponent": - if (value.Length < 11) + + if (resourceComponent != null) { - valid = true; + // Check if the name mathces the length requirements for the component + if ((value.Length >= (Convert.ToInt32(resourceComponent.MinLength)) && (value.Length <= Convert.ToInt32(resourceComponent.MaxLength)))) + { + valid = true; + } } - break; + } + } + catch (Exception ex) + { + AdminLogService.PostItem(new AdminLogMessage() { Title = "ERROR", Message = ex.Message }); } - return valid; } diff --git a/ready/AzNamingTool/INSTALLATION.md b/ready/AzNamingTool/INSTALLATION.md deleted file mode 100644 index 0428737f..00000000 --- a/ready/AzNamingTool/INSTALLATION.md +++ /dev/null @@ -1,433 +0,0 @@ -[Overview](./) | [Installation](INSTALLATION.md) | [Updating](UPDATING.md) | [Using the API](USINGTHEAPI.md) | [Release Notes](RELEASENOTES.md) | [Version History](VERSIONHISTORY.md) | [FAQ](FAQ.md) | [Contributors](CONTRIBUTORS.md) - -# Azure Naming Tool v2 - Installation - -[Choosing an Installation Option](#choosing-an-installation-option) - -[How To Install](#how-to-install) - -* [Run as a Docker image](#run-as-a-docker-image) (Local development) - -* [Run as a Docker image with podman](#run-as-a-docker-image-with-podman) (Local development) - -* [Run as an Azure Web App Using GitHub Action](#run-as-an-azure-web-app-using-github-action) (.NET Application running in an Azure App Service, non-container) - -* [Run as a Web App for Containers](#run-as-a-web-app-for-containers) (Single container running in an Azure App Service) - -* [Run as an Azure Container App](#run-as-an-azure-container-app) (Single container running in an Azure Container App) - -* [Run as a Stand-Alone Site](#run-as-a-stand-alone-site) - -*** -## Choosing An Installation Option -The Azure Naming Tool was designed to be deployed in nearly any environment. This includes as a stand-alone application, or as a container. Each deployment option offers pros/cons, depending on your environment and level of experience. Here is a break-down of the deployment options: - -* [**Run as a Docker image**](#run-as-a-docker-image) - * Ideal for local deployments - * Requires docker engine installed in environment - * Requires storage volume mount - -* [**Run as a Docker image with podman**](#run-as-a-docker-image-with-podman) - * Ideal for local deployments - * Requires podman installed in environment - * Requires storage volume mount - -* [**Run as an Azure Web App Using GitHub Action**](#run-as-an-azure-web-app-using-github-action) - * Ideal for fastest deployment - * Requires Azure Web App - * Utilizes provided GitHub Action for deployment - * Requires GitHub secrets to be created (instructions in GitHub Action workflow file) - * Integrated for continuous deployment from GitHub - -* [**Run as a Web App for Containers**](#run-as-a-web-app-for-containers) - * Ideal for single container installations - * Requires Azure App Service - * Requires Azure Storage account / Azure Files Fileshare for persistent storage - -* [**Run as an Azure Container App**](#run-as-an-azure-container-app) - * Ideal for multiple container installations (integration with other containers, services, etc.) - * Requires Azure Container App - * Requires Azure Storage account / Azure Files Fileshare for persistent storage - -* [**Run as a Stand-Alone Site**](#run-as-a-stand-alone-site) - * Ideal for legacy deployments - * Requires web server deployment (IIS, Apache, etc.) - -*** -## How To Install - -This project contains a .NET 6 application, with Docker support. To use, complete the following: - -> **NOTE:** -> The Azure Naming Tool requires persistent storage for the configuration files when run as a container. The following processes will explain how to create this volume in your respective environment. All configuration JSON files will be stored in the volume to ensure the configuration is persisted. - -### Run as a Docker image - -This process will allow you to deploy the Azure Naming Tool using Docker to your local environment. - -1. Scroll up to the top, left corner of this page. -2. Click on the **CloudAdoptionFramework** link to open the root of this repository. -3. Click the green **<>Code** button and select **Download ZIP**. -4. Open your Downloads folder using File Explorer. -5. Extract the contents of the ZIP archive. - -> **NOTE:** -> Validate the project files extracted successfully and match the contents in the GitHub repository. - -6. Open a **Command Prompt** -7. Change the directory to the **AzNamingTool** folder. For example: - -```cmd -cd .\Downloads\CloudAdoptionFramework-master\CloudAdoptionFramework-master\ready\AzNamingTool -``` - -8. Run the following **Docker command** to build the image: - -```cmd -docker build -t azurenamingtool . -``` - -> **NOTE:** -> Ensure the '.' is included in the command - -9. Run the following **Docker command** to create a new container and mount a new volume: - -```cmd -docker run -d -p 8081:80 --mount source=azurenamingtoolvol,target=/app/settings azurenamingtool:latest -``` - -> **NOTES:** -> * Substitute 8081 for any port not in use on your machine -> * You may see warnings in the command prompt regarding DataProtection and keys. These indicate that the keys are not persisted and are only local to the container instances. - -10. Access the site using the following URL: *http://localhost:8081* - -> **NOTE:** -> Substitute 8081 for the port you used in the docker run command - -*** -### Run as a Docker image with podman - -This process will allow you to deploy the Azure Naming Tool using Docker to your local environment. - -1. Scroll up to the top, left corner of this page. -2. Click on the **CloudAdoptionFramework** link to open the root of this repository. -3. Click the green **<>Code** button and select **Download ZIP**. -4. Open your Downloads folder using File Explorer. -5. Extract the contents of the ZIP archive. - -> **NOTE:** -> Validate the project files extracted successfully and match the contents in the GitHub repository. -6. Open a **Command Prompt** -7. Change the directory to the **AzNamingTool** folder. For example: - -```cmd -cd .\Downloads\CloudAdoptionFramework-master\CloudAdoptionFramework-master\ready\AzNamingTool -``` - -8. Run the following **Docker command** to build the image: - -```cmd -podman build -t azurenamingtool . -``` - -> **NOTE:** -> Ensure the '.' is included in the command -9. Run the following **Docker command** to create a new container and mount a new volume: - -```cmd -podman run -d -p 8081:8081 --mount type=volume,source=azurenamingtoolvol,target=/app/settings -e "ASPNETCORE_URLS=http://+:8081" azurenamingtool:latest -``` - -> **NOTES:** -> * Substitute 8081 for any port not in use on your machine -> * You may see warnings in the command prompt regarding DataProtection and keys. These indicate that the keys are not persisted and are only local to the container instances. -10. Access the site using the following URL: *http://localhost:8081* - -> **NOTE:** -> Substitute 8081 for the port you used in the docker run command - -*** -### Run as an Azure Web App Using GitHub Action - -(.NET application, non-container) - -This process will allow you to deploy the Azure Naming Tool as a .NET application as an Azure Web App. This is the fastest deployment option and allows you to deploy and utilize your installation in minutes. This process includes **Creating a fork of the repository**, **Creating an Azure Web App**, and **Enabling Authentication**. The provided GitHub Action will deploy your repository code on every commit. - -**Fork the Cloud Adoption Framework Repository** -1. Scroll up to the top-left corner of this page. -2. Click on the **CloudAdoptionFramework** link to open the root of this repository. -3. Click the **Fork** option in the top-right menu. -4. Select your desired **Owner** and **Repository name** and click **Create fork**. -5. Click the green **<>Code** button -6. Click the **.github/workflows** link. - - ![Run as Azure Web App 1](./wwwroot/Screenshots/RunAsWebApp1.png) - -7. Click the **.deploy-azure-naming-tool-to-azure-webapps-dotnet-core.yml** link. - - ![Run as Azure Web App 2](./wwwroot/Screenshots/RunAsWebApp2.png) - -8. Review the instructions for creating the required GitHub secrets. - -> **NOTES:** -> The GitHub Action will not successfully deploy until the secrets are created. -> You must create an Azure Web App and configure the GitHub Action secrets to deploy to your Azure Web App. - -**Create an Azure Web App** (if needed) -For an automated deployment of a Web App, utilize the button below and fill in the required information. Then proceed to step 4. - -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2FCloudAdoptionFramework%2Fmaster%2Fready%2FAzNamingTool%2FDeployments%2FAppService-WebApp%2Fsolution.json) -[![Deploy to Azure Gov](https://aka.ms/deploytoazuregovbutton)](https://portal.azure.us/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2FCloudAdoptionFramework%2Fmaster%2Fready%2FAzNamingTool%2FDeployments%2FAppService-WebApp%2Fsolution.json) - -1. Create a new Azure Web App in the Azure portal. -2. For the **Publish** option, select **Code**. -3. For the **Runtime stack**, select **.NET 6**. - - ![Web App Basics](./wwwroot/Screenshots/WebAppInstallation1.png) - -4. Download the **Publish Profile** for use within the GitHub Action secret. - -```PowerShell -Get-AzWebApp -Name | Get-AzWebAppPublishingProfile -OutputFile | Out-Null -``` - -**OR** - - ![Web App Details](./wwwroot/Screenshots/WebAppInstallation2.png) - -**Enable Azure Web App Authentication** (using Azure AD) - -1. In the Azure Portal for your Azure Web App, Navigate to the **Authentication** blade. -2. Select **Add identity provider**. -3. In the **Identity provider** section, select **Microsoft**. -4. Enter the desired **Name**. All other options can be left as default. -5. Click **Add**. - - ![Web App Authentication](./wwwroot/Screenshots/WebAppAuthentication1.png) - -**Get Azure Web App Credentials** -1. In a command prompt, execute the following command to create credentials for the Azure Web App: - -``` -az ad sp create-for-rbac --name "[YOUR CREDENTIAL NAME]" --role contributor --scopes /subscriptions/[YOUR SUBSCRIPTION ID]/resourceGroups/[YOUR RESORUCE GROUP NAME] --sdk-auth -``` - -3. Copy the returned value for later use. - -``` JSON -{ - "clientId": "[YOUR CLIENT ID]", - "clientSecret": "[YOUR CLIENT SECRET]", - "subscriptionId": "[YOUR SUBSCRIPTION ID]", - "tenantId": "[YOUR TENANT ID]", - "activeDirectoryEndpointUrl": "https://login.microsoftonline.com", - "resourceManagerEndpointUrl": "https://management.azure.com/", - "activeDirectoryGraphResourceId": "https://graph.windows.net/", - "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/", - "galleryEndpointUrl": "https://gallery.azure.com/", - "managementEndpointUrl": "https://management.core.windows.net/" -} -``` - -**Create GitHub Secrets** -1. In your GitHub repository, click **Settings** in the top menu. -2. Click **Secrets** in the left menu. -3. Click **New repository secret**. -4. Enter **AZURE_WEBAPP_PUBLISH_PROFILE** as the **Name**. -5. Enter the **Publish Profile** data for your Azure Web App as the **Value**. -6. Click **Add secret**. -7. Click **New repository secret**. -8. Enter **AZURE_WEBAPP_NAME** as the **Name**. -9. Enter the name of your Azure Web App as the **Value**. -10. Click **Add secret**. -11. Click **New repository secret**. -12. Enter **AZURE_CREDENTIALS** as the **Name**. -13. Enter the **Azure Wwb App Credentials** JSON as the **Value**: - - ![GitHub Secrets](./wwwroot/Screenshots/GitHubActionInstallation1.png) - -**Enable GitHub Workflow** - -1. In your GitHub reposiutory, click the **Actions** tab. -2. Click on **I understand my workflows, go ahead and enable them**. -3. Select the **Azure Naming Tool - Build and deploy to an Azure Web App** workflow in the left navigation. -4. Click **Run workflow**. -5. Confirm the workflow completes successfully. - - ![GitHub Workflow](./wwwroot/Screenshots/GitHubActionInstallation2.png) - -6. Access your Azure Web App to confirm the site is successfully deployed. - -*** - -### Run as a Web App for Containers -(App Service running a container) - -The Azure Naming Tool requires persistent storage for the configuration files when run as a container. The following processes will explain how to create this volume for your Azure App Service Container. All configuration JSON files will be stored in the volume to ensure the configuration is persisted. - -> **NOTE:** -> For many of the steps, a sample process is provided. However, there are many ways to accomplish each step. - -1. Scroll up to the top-left corner of this page. -2. Click on the **CloudAdoptionFramework** link to open the root of this repository. -3. Click the green **<>Code** button and select **Download ZIP**. -4. Open your Downloads folder using File Explorer. -5. Extract the contents of the ZIP archive. - -> **NOTE:** -> Validate the project files extracted successfully and match the contents in the GitHub repository. - -6. Open a **Command Prompt**. -7. Change the directory to the **AzNamingTool** folder. For example: - -```cmd -cd .\Downloads\CloudAdoptionFramework-master\CloudAdoptionFramework-master\ready\AzNamingTool -``` - -8. Run the following **Docker command** to build the image: - -```cmd -docker build -t azurenamingtool . -``` - -> **NOTE:** -> Ensure the '.' is included in the command - -9. Create an Azure Container Registry: [Microsoft Docs reference](https://docs.microsoft.com/azure/container-registry/container-registry-get-started-portal#:~:text=%20Quickstart%3A%20Create%20an%20Azure%20container%20registry%20using,must%20log%20in%20to%20the%20registry...%20More%20) -10. Build and publish your image to the Azure Container Registry: [Microsoft Docs reference](https://docs.microsoft.com/azure/container-registry/container-registry-get-started-docker-cli?tabs=azure-cli) -11. Create an Azure Files file share for persistent storage: [Microsoft Docs reference](https://docs.microsoft.com/azure/storage/files/storage-how-to-create-file-share?tabs=azure-portal) - - ![FileShare](./wwwroot/Screenshots/FileShare.png) - -12. Create an Azure App Service - Web App: [Microsoft Docs reference](https://docs.microsoft.com/azure/app-service/quickstart-custom-container?tabs=dotnet&pivots=container-linux) -13. Mount the file share as local storage for the Azure App Service: [Microsoft Docs reference](https://docs.microsoft.com/azure/app-service/configure-connect-to-azure-storage?tabs=portal&pivots=container-linux) - - ![MountStorage](./wwwroot/Screenshots/MountStorage.png) - -14. Deploy the image from the Azure Container Registry to the Azure App Service: [Microsoft Docs reference](https://docs.microsoft.com/azure/app-service/deploy-ci-cd-custom-container?tabs=acr&pivots=container-linux) -15. Access the site using your Azure App Service URL. - -> **NOTE:** -> It is recommended that you enable authentication on your Container App to prevent un-authorized access. [Authentication and authorization in Azure Container Apps](https://docs.microsoft.com/azure/container-apps/authentication) - -*** - -### Run as an Azure Container App - -The Azure Naming Tool requires persistent storage for the configuration files when run as a container. The following processes will explain how to create this volume for your Azure App Service Container. All configuration JSON files will be stored in the volume to ensure the configuration is persisted. - -> **NOTE:** -> For many of the steps, a sample process is provided, however, there are many ways to accomplish each step. - -1. Scroll up to the top, left corner of this page. -2. Click on the **CloudAdoptionFramework** link to open the root of this repository. -3. Click the green **<>Code** button and select **Download ZIP**. -4. Open your Downloads folder using File Explorer. -5. Extract the contents of the ZIP archive. - -> **NOTE:** -> Validate the project files extracted successfully and match the contents in the GitHub repository. - -6. Open a **Command Prompt** -7. Change the directory to the **AzNamingTool** folder. For example: - -```cmd -cd .\Downloads\CloudAdoptionFramework-master\CloudAdoptionFramework-master\ready\AzNamingTool -``` - -8. Run the following **Docker command** to build the image: - -```cmd -docker build -t azurenamingtool . -``` - -> **NOTE:** -> Ensure the '.' is included in the command - -9. Create an Azure Container Registry: [Microsoft Docs reference](https://docs.microsoft.com/azure/container-registry/container-registry-get-started-portal#:~:text=%20Quickstart%3A%20Create%20an%20Azure%20container%20registry%20using,must%20log%20in%20to%20the%20registry...%20More%20) -10. Build and publish your image to the Azure Container Registry: [Microsoft Docs reference](https://docs.microsoft.com/azure/container-registry/container-registry-get-started-docker-cli?tabs=azure-cli) -11. Create an Azure Files file share for persistent storage: [Microsoft Docs reference](https://docs.microsoft.com/azure/storage/files/storage-how-to-create-file-share?tabs=azure-portal) - - ![FileShare](./wwwroot/Screenshots/FileShare.png) - -12. Create an Azure Container App: [Quickstart: Deploy an existing container image with the Azure CLI](https://docs.microsoft.com/azure/container-apps/get-started-existing-container-image?tabs=bash&pivots=container-apps-public-registry) - -> **NOTE:** -> It is possible to deploy a container app via the portal, however, setting the volume for persistent storage is much easier using the CLI. - -13. Configure Container App to use Azure Storage File share for volume: [Use storage mounts in Azure Container Apps](https://docs.microsoft.com/azure/container-apps/storage-mounts?pivots=aca-cli#azure-files) -15. Access the site using your Azure App Service URL. - -> **NOTE:** -> It is recommended that you enable authentication on your Container App to prevent un-authorized access. [Authentication and authorization in Azure Container Apps](https://docs.microsoft.com/azure/container-apps/authentication) - -*** - -### Run as an Azure App Service Container - -The Azure Naming Tool requires persistent storage for the configuration files when run as a container. The following processes will explain how to create this volume for your Azure App Service Container. All configuration JSON files will be stored in the volume to ensure the configuration is persisted. - -> **NOTE:** -> For many of the steps, a sample process is provided, however, there are many ways to accomplish each step. - -1. Scroll up to the top, left corner of this page. -2. Click on the **CloudAdoptionFramework** link to open the root of this repository. -3. Click the green **<>Code** button and select **Download ZIP**. -4. Open your Downloads folder using File Explorer. -5. Extract the contents of the ZIP archive. - -> **NOTE:** -> Validate the project files extracted successfully and match the contents in the GitHub repository. - -6. Open a **Command Prompt** -7. Change the directory to the **AzNamingTool** folder. For example: - -```cmd -cd .\Downloads\CloudAdoptionFramework-master\CloudAdoptionFramework-master\ready\AzNamingTool -``` - -8. Run the following **Docker command** to build the image: - -```cmd -docker build -t azurenamingtool . -``` - -> **NOTE:** -> Ensure the '.' is included in the command - -9. Create an Azure Container Registry: [Microsoft Docs reference](https://docs.microsoft.com/azure/container-registry/container-registry-get-started-portal#:~:text=%20Quickstart%3A%20Create%20an%20Azure%20container%20registry%20using,must%20log%20in%20to%20the%20registry...%20More%20) -10. Build and publish your image to the Azure Container Registry: [Microsoft Docs reference](https://docs.microsoft.com/azure/container-registry/container-registry-get-started-docker-cli?tabs=azure-cli) -11. Create an Azure Files file share for persistent storage: [Microsoft Docs reference](https://docs.microsoft.com/azure/storage/files/storage-how-to-create-file-share?tabs=azure-portal) - - ![FileShare](./wwwroot/Screenshots/FileShare.png) - -12. Create an Azure App Service - Web App: [Microsoft Docs reference](https://docs.microsoft.com/azure/app-service/quickstart-custom-container?tabs=dotnet&pivots=container-linux) -13. Mount the file share as local storage for the Azure App Service: [Microsoft Docs reference](https://docs.microsoft.com/azure/app-service/configure-connect-to-azure-storage?tabs=portal&pivots=container-linux) - - ![MountStorage](./wwwroot/Screenshots/MountStorage.png) - -14. Deploy the image from the Azure Container Registry to the Azure App Service: [Microsoft Docs reference](https://docs.microsoft.com/azure/app-service/deploy-ci-cd-custom-container?tabs=acr&pivots=container-linux) -15. Access the site using your Azure App Service URL. - -> **NOTE:** -> It is recommended that you enable authentication on your App Service to prevent un-authorized access. [Authentication and authorization in Azure App Service and Azure Functions](https://docs.microsoft.com/azure/app-service/overview-authentication-authorization) - -*** - -### Run as a Stand-Alone Site - -The Azure Naming Tool can be installed as a stand-alone .NET Core application. The installation process will vary, depending on your hosting environment. - -To install as a stand-alone site: - -1. Scroll up to the top, left corner of this page. -2. Click on the **CloudAdoptionFramework** link to open the root of this repository. -3. Click the green **<>Code** button and select **Download ZIP**. -4. Open your Downloads folder using File Explorer. -5. Extract the contents of the ZIP archive. - -> **NOTE:** -> Validate the project files extracted successfully and match the contents in the GitHub repository. - -6. In your IIS/Apache environment, create a new .NET application with the Azure Naming Tool source for the directory diff --git a/ready/AzNamingTool/Models/AdminLogMessage.cs b/ready/AzNamingTool/Models/AdminLogMessage.cs index d6633858..513cefff 100644 --- a/ready/AzNamingTool/Models/AdminLogMessage.cs +++ b/ready/AzNamingTool/Models/AdminLogMessage.cs @@ -6,5 +6,6 @@ public class AdminLogMessage public DateTime CreatedOn { get; set; } = DateTime.Now; public string Title { get; set; } public string Message { get; set; } + public string? Source { get; set; } = "System"; } } \ No newline at end of file diff --git a/ready/AzNamingTool/Models/AdminUser.cs b/ready/AzNamingTool/Models/AdminUser.cs new file mode 100644 index 00000000..136bf148 --- /dev/null +++ b/ready/AzNamingTool/Models/AdminUser.cs @@ -0,0 +1,8 @@ +namespace AzureNamingTool.Models +{ + public class AdminUser + { + public int Id { get; set; } + public string? Name { get; set; } + } +} diff --git a/ready/AzNamingTool/Models/ConfigurationData.cs b/ready/AzNamingTool/Models/ConfigurationData.cs index fa0b9121..f585675a 100644 --- a/ready/AzNamingTool/Models/ConfigurationData.cs +++ b/ready/AzNamingTool/Models/ConfigurationData.cs @@ -18,13 +18,16 @@ public class ConfigurationData public List CustomComponents { get; set; } public List GeneratedNames { get; set; } public List AdminLogs { get; set; } + public List AdminUsers { get; set; } public string? SALTKey { get; set; } public string? AdminPassword { get; set; } public string? APIKey { get; set; } public string? DismissedAlerts { get; set; } - public string? DuplicateNamesAllowed { get; set; } = "false"; - public string? GenerationWebhook { get; set; } = string.Empty; - public string? ConnectivityCheckEnabled { get; set; } = "true"; + public string? DuplicateNamesAllowed { get; set; } + public string? GenerationWebhook { get; set; } + public string? ConnectivityCheckEnabled { get; set; } + public string? IdentityHeaderName { get; set; } + public string? ResourceTypeEditingAllowed { get; set; } } } diff --git a/ready/AzNamingTool/Models/CustomComponent.cs b/ready/AzNamingTool/Models/CustomComponent.cs index 505d82eb..c1b119c9 100644 --- a/ready/AzNamingTool/Models/CustomComponent.cs +++ b/ready/AzNamingTool/Models/CustomComponent.cs @@ -17,5 +17,7 @@ public string ShortName set => _ShortName = value?.ToLower(); // set method } public int SortOrder { get; set; } = 0; + public string MinLength { get; set; } = "1"; + public string MaxLength { get; set; } = "10"; } } diff --git a/ready/AzNamingTool/Models/GeneratedName.cs b/ready/AzNamingTool/Models/GeneratedName.cs index 1dfc19b3..3e64da1f 100644 --- a/ready/AzNamingTool/Models/GeneratedName.cs +++ b/ready/AzNamingTool/Models/GeneratedName.cs @@ -7,5 +7,6 @@ public class GeneratedName public string ResourceName { get; set; } public string? ResourceTypeName { get; set; } public List Components { get; set; } + public string? User { get; set; } = "General"; } } diff --git a/ready/AzNamingTool/Models/IdentityProviderDetails.cs b/ready/AzNamingTool/Models/IdentityProviderDetails.cs new file mode 100644 index 00000000..ae07cdc7 --- /dev/null +++ b/ready/AzNamingTool/Models/IdentityProviderDetails.cs @@ -0,0 +1,8 @@ +namespace AzureNamingTool.Models +{ + public class IdentityProviderDetails + { + public string CurrentUser { get; set; } = "System"; + public string? CurrentIdentityProvider { get; set; } + } +} diff --git a/ready/AzNamingTool/Models/ResourceComponent.cs b/ready/AzNamingTool/Models/ResourceComponent.cs index e7b2f39f..2b271269 100644 --- a/ready/AzNamingTool/Models/ResourceComponent.cs +++ b/ready/AzNamingTool/Models/ResourceComponent.cs @@ -14,5 +14,7 @@ public class ResourceComponent public int SortOrder { get; set; } = 0; public bool IsCustom { get; set; } = false; public bool IsFreeText { get; set; } = false; + public string? MinLength { get; set; } + public string? MaxLength { get; set; } } } diff --git a/ready/AzNamingTool/Models/ResourceNameRequest.cs b/ready/AzNamingTool/Models/ResourceNameRequest.cs index cb7e513a..7614a29e 100644 --- a/ready/AzNamingTool/Models/ResourceNameRequest.cs +++ b/ready/AzNamingTool/Models/ResourceNameRequest.cs @@ -18,5 +18,6 @@ public class ResourceNameRequest /// long - Resource Id (example: 14) /// public long? ResourceId { get; set; } = 0; + public string? CreatedBy { get; set; } = "System"; } } diff --git a/ready/AzNamingTool/Models/ResourceNameResponse.cs b/ready/AzNamingTool/Models/ResourceNameResponse.cs index 6c1075a2..21e7e7cc 100644 --- a/ready/AzNamingTool/Models/ResourceNameResponse.cs +++ b/ready/AzNamingTool/Models/ResourceNameResponse.cs @@ -5,5 +5,6 @@ public class ResourceNameResponse public string ResourceName { get; set; } public string Message { get; set; } public bool Success { get; set; } + public GeneratedName? resourceNameDetails { get; set; } } } diff --git a/ready/AzNamingTool/Models/ServicesData.cs.cs b/ready/AzNamingTool/Models/ServicesData.cs.cs index 7c0251b6..ddb404cc 100644 --- a/ready/AzNamingTool/Models/ServicesData.cs.cs +++ b/ready/AzNamingTool/Models/ServicesData.cs.cs @@ -14,5 +14,6 @@ public class ServicesData public List? CustomComponents { get; set; } public List? GeneratedNames { get; set; } public List? AdminLogMessages { get; set; } + public List? AdminUsers { get; set; } } } diff --git a/ready/AzNamingTool/Models/SiteConfiguration.cs b/ready/AzNamingTool/Models/SiteConfiguration.cs index 1fc20315..24c60230 100644 --- a/ready/AzNamingTool/Models/SiteConfiguration.cs +++ b/ready/AzNamingTool/Models/SiteConfiguration.cs @@ -8,8 +8,10 @@ public class SiteConfiguration public string? AppTheme { get; set; } public bool? DevMode { get; set; } = false; public string? DismissedAlerts { get; set; } - public string? DuplicateNamesAllowed { get; set; } = "false"; - public string? GenerationWebhook { get; set; } = string.Empty; - public string? ConnectivityCheckEnabled { get; set; } = "true"; + public string? DuplicateNamesAllowed { get; set; } + public string? GenerationWebhook { get; set; } + public string? ConnectivityCheckEnabled { get; set; } + public string? IdentityHeaderName { get; set; } + public string? ResourceTypeEditingAllowed { get; set; } } } diff --git a/ready/AzNamingTool/Models/StateContainer.cs b/ready/AzNamingTool/Models/StateContainer.cs index 508be5d9..3bda57c2 100644 --- a/ready/AzNamingTool/Models/StateContainer.cs +++ b/ready/AzNamingTool/Models/StateContainer.cs @@ -8,6 +8,7 @@ public class StateContainer private string? _apptheme; private bool? _newsenabled; public bool _reloadnav; + public bool? _configurationdatasynced; public bool Verified { @@ -59,7 +60,7 @@ public void SetPassword(bool password) public string AppTheme { - get => _apptheme ?? "bg-default text-black"; + get => _apptheme ?? "bg-default text-dark"; set { _apptheme = value; @@ -95,6 +96,21 @@ public void SetNavReload(bool reloadnav) NotifyStateChanged(); } + public bool ConfigurationDataSynced + { + get => _configurationdatasynced ?? false; + set + { + _configurationdatasynced = value; + NotifyStateChanged(); + } + } + + public void SetConfigurationDataSynced(bool configurationdatasynced) + { + _configurationdatasynced = configurationdatasynced; + NotifyStateChanged(); + } public event Action? OnChange; diff --git a/ready/AzNamingTool/Pages/Admin.razor b/ready/AzNamingTool/Pages/Admin.razor index cddaee61..52a8b858 100644 --- a/ready/AzNamingTool/Pages/Admin.razor +++ b/ready/AzNamingTool/Pages/Admin.razor @@ -37,21 +37,21 @@ @if (!admin) {

- Enter the Admin Password to configure the Azure Naming Tool site. + Enter the Global Admin Password to configure the Azure Naming Tool site.

- +
} else {

- This form allows you to set the Admin password, API key, and configuration for the Azure Naming Tool. + This page allows you to security settings, view cache data, and site configurations for the Azure Naming Tool.

-
- @@ -63,63 +63,208 @@ { }
-
-
-
-

- Enter a new admin password for the site. -

-

Requirements

-
    -
  • Contain a number
  • -
  • Contain one upper case letter
  • -
  • Be at least 8 characters
  • -
-
- - -
+
+ @if (currentuser == "GlobalAdmin") + { +
+ +
+

+ Enter a new Global Admin password for the site. +

+

Requirements

+
    +
  • Contain a number
  • +
  • Contain one upper case letter
  • +
  • Be at least 8 characters
  • +
+
+ + +
+
+
+
+ +
+

+ The current API key is displayed. Click Generate to generate a new random API Key, or update the text to the desired value. +

+
+ + + +
+
+
+ } + else + { +
+ Log in with the Global Admin password to update these settings. +
+ }
-
-
-
-

- The current API key is displayed. Click Generate to generate a new random API Key, or update the text to the desired value. -

-
- - - -
+
+ @if (currentuser == "GlobalAdmin") + { +

+ This section is used to configure the Identity Provider settings for the site if authentication is implemented. The Azure Naming Tool is designed to work with Azure App Service Authentication, by default. If the Azure Naming Tool is configured to identify users, the site will track user activity. Users can also be designated as Admins using the Admin Users configuration. +

+

+ You can learn more about the new Identity Provider Integration HERE. +

+ @if (!String.IsNullOrEmpty(currentidentityprovider)) + { +
+ +
+ } +
+ +
+

+ This setting is used to identify site users when authenticating using an Identity provider. Authentication using an Identity provider will often inject headers into the request. The site will check for the specified header name and assign the value as the user's identity. This value will be used to log user activity. +

+
+ + +
+ +
+
+
+ +
+
+

+ When authenticating users with an Identity provider, the Identity Header Name setting can be used to idenity site users. Enter the user id below to assign the user as an Admin for the Azure Naming Tool. +

+ @if (!String.IsNullOrEmpty(currentidentityprovider)) + { + @switch (currentidentityprovider.ToLower()) + { + case "aad": + + break; + case "github": + + break; + case "google": + + break; + } + } +
+
+ +
+ @if (servicesData.AdminUsers.Count > 0) + { +
+ + + + + + + + + @foreach (var item in @servicesData.AdminUsers) + { + + + + + } + +
NameActions
@item.Name + @if (GeneralHelper.NormalizeName(currentuser, true) != GeneralHelper.NormalizeName(@item.Name, true)) + { + + } +
+
+ } + else + { +

There currently no assigned admin users.

+ } +

+ NOTE
+ Added users will need to close their browser and re-open the site for the change to take effect. +

+ +
+
+ } + else + { +
+ Log in with the Global Admin password to update these settings. +
+ }
-
-
-
+

The tool uses caching for data to improve performance. Click Clear to clear all cached data.

@@ -133,14 +278,14 @@
-
-
- @@ -159,9 +304,31 @@
+
-
- +
+
+

+ By default, Resource Type Validation is configured to match the Azure portal. These values can be overriden by enabling the following setting. +

+
+
+ + Enable +
+
+
+
+ @@ -170,10 +337,6 @@

The Azure Naming Tool will verify the tool has internet connectivity to enable update features. Use this setting to disable the connectivity check functionality.

-

- NOTE
- Disabling this setting may result in connectivity errors being logged to the Admin Log. -

-
- @@ -229,10 +392,14 @@ protected ThemeInfo? theme { get; set; } [CascadingParameter] public IModalService Modal { get; set; } + [CascadingParameter] + private IdentityProviderDetails? identityProviderDetails { get; set; } private ServicesData servicesData = new(); private string? currentpassword; private string? newpassword; private string? currentapikey; + private string? currentidentityheadername; + private string? currentidentityprovider; private ResponseMessage message = new(); private bool admin; private bool versionalertshown = false; @@ -241,10 +408,12 @@ private bool dismissalert = false; private bool duplicatenamesallowed = false; private bool connectivitycheckenabled = true; + private bool resourcetypeeditingallowed = false; private string currentgenerationwebhook = string.Empty; private string versionalert = string.Empty; private string? appversion = Assembly.GetEntryAssembly().GetCustomAttribute().InformationalVersion; private SiteConfiguration config = ConfigurationHelper.GetConfigurationData(); + private string? currentuser; protected override async void OnInitialized() { @@ -252,8 +421,10 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { + currentuser = await IdentityHelper.GetCurrentUser(session); if (firstRender) { + servicesData = await ServicesHelper.LoadServicesData(servicesData, true); var result = await session.GetAsync("admin"); admin = (bool)result.Value; if (admin) @@ -269,7 +440,16 @@ } duplicatenamesallowed = Convert.ToBoolean(ConfigurationHelper.GetAppSetting("DuplicateNamesAllowed")); connectivitycheckenabled = Convert.ToBoolean(ConfigurationHelper.GetAppSetting("ConnectivityCheckEnabled")); + resourcetypeeditingallowed = Convert.ToBoolean(ConfigurationHelper.GetAppSetting("ResourceTypeEditingAllowed")); currentgenerationwebhook = ConfigurationHelper.GetAppSetting("GenerationWebhook", true); + currentidentityheadername = ConfigurationHelper.GetAppSetting("IdentityHeaderName", true); + if (identityProviderDetails != null) + { + if (!String.IsNullOrEmpty(identityProviderDetails.CurrentIdentityProvider)) + { + currentidentityprovider = identityProviderDetails.CurrentIdentityProvider; + } + } } await storage.SetAsync("apptheme", theme.ThemeStyle); @@ -279,13 +459,14 @@ } } - private async void AdminFormAction(string action) + private async void AdminFormAction(string action, int id = 0) { message = new ResponseMessage(); message.Header = "INFORMATION"; message.Type = MessageTypesEnum.INFORMATION; ServiceResponse serviceResponse = new(); bool redirect = false; + bool confirm = false; switch (action) { @@ -295,11 +476,12 @@ { state.SetAdmin(true); await session.SetAsync("admin", true); + await session.SetAsync("currentuser", "GlobalAdmin"); // Load the current API Key currentapikey = GeneralHelper.DecryptString(config.APIKey, config.SALTKey); message.Type = MessageTypesEnum.INFORMATION; message.Header = "INFORMATION"; - message.Message = "Admin logged in."; + message.Message = "Global Admin logged in."; admin = true; redirect = true; } @@ -309,7 +491,7 @@ await session.SetAsync("admin", false); message.Type = MessageTypesEnum.ERROR; message.Header = "ERROR"; - message.Message = "Login failed"; + message.Message = "Login failed!"; } break; case "logout": @@ -317,11 +499,11 @@ await session.SetAsync("admin", false); message.Type = MessageTypesEnum.INFORMATION; message.Header = "INFORMATION"; - message.Message = "Admin logged out."; + message.Message = "User Logged out."; redirect = true; break; case "password": - bool confirmPassword = await JsRuntime.InvokeAsync("confirm", "This will change the admin password. Are you sure?"); + bool confirmPassword = await JsRuntime.InvokeAsync("confirm", "This will change the Global Admin Password. Are you sure?"); if (confirmPassword) { // Set the new admin password @@ -330,13 +512,32 @@ { message.Type = MessageTypesEnum.SUCCESS; message.Header = "SUCCESS"; - message.Message = "Password updated!"; + message.Message = "Global Admin Password updated!"; } else { message.Type = MessageTypesEnum.ERROR; message.Header = "ERROR"; - message.Message = "There was a problem updating the password!"; + message.Message = "There was a problem updating the Global Admin Password!"; + } + } + break; + case "identityheadernamesave": + bool confirmIdentityHeaderNameSave = await JsRuntime.InvokeAsync("confirm", "This will update the current Identity Header Name. Are you sure?"); + if (confirmIdentityHeaderNameSave) + { + serviceResponse = await AdminService.UpdateIdentityHeaderName(currentidentityheadername); + if (serviceResponse.Success) + { + message.Type = MessageTypesEnum.SUCCESS; + message.Header = "SUCCESS"; + message.Message = "Identity Header Name updated!"; + } + else + { + message.Type = MessageTypesEnum.ERROR; + message.Header = "ERROR"; + message.Message = "There was a problem updating the Identity Header Name!"; } } break; @@ -367,7 +568,6 @@ if (serviceResponse.Success) { currentapikey = serviceResponse.ResponseObject; - await JsRuntime.InvokeAsync("console.log", currentapikey); message.Type = MessageTypesEnum.SUCCESS; message.Header = "SUCCESS"; message.Message = "API Key updated!"; @@ -401,12 +601,7 @@ } break; case "clearcache": - ObjectCache memoryCache = MemoryCache.Default; - List cacheKeys = memoryCache.Select(kvp => kvp.Key).ToList(); - foreach (string cacheKey in cacheKeys) - { - memoryCache.Remove(cacheKey); - } + CacheHelper.ClearAllCache(); message.Type = MessageTypesEnum.SUCCESS; message.Header = "SUCCESS"; message.Message = "Cache cleared!"; @@ -420,13 +615,34 @@ await ShowInformationModal("bg-navcolor", "Sample Post", "

This is a sample Generation Webhook post.

" + webhookdata + ""); break; + case "adminuseradd": + ShowAddModal(id, "AdminUser", "Add Admin User", "

Add an Admin User.

"); + break; + case "adminuserdelete": + confirm = await JsRuntime.InvokeAsync("confirm", "Are you sure?"); + if (confirm) + { + serviceResponse = await AdminUserService.DeleteItem(Convert.ToInt32(id)); + if (serviceResponse.Success) + { + message.Type = MessageTypesEnum.SUCCESS; + message.Message = "Admin User deleted!"; + } + else + { + message.Type = MessageTypesEnum.ERROR; + message.Message = "Admin User deletion failed!"; + message.MessageDetails = serviceResponse.ResponseMessage; + } + } + break; } config = ConfigurationHelper.GetConfigurationData(); if (message.Message != null) { - switch (message.Type) + switch (message.Type) { case MessageTypesEnum.INFORMATION: toastService.ShowInfo(message.Message); @@ -441,9 +657,12 @@ toastService.ShowError(message.Message); break; } - AdminLogService.PostItem(new AdminLogMessage() { Title = message.Type.ToString(), Message = message.Message }); + AdminLogService.PostItem(new AdminLogMessage() { Title = message.Type.ToString(), Message = message.Message, Source = currentuser }); } + + servicesData = await ServicesHelper.LoadServicesData(servicesData, admin); + state.SetNavReload(true); StateHasChanged(); if (redirect) @@ -479,6 +698,9 @@ disabled = true; } break; + case "identityheadername": + currentidentityheadername = value; + break; case "apikey": currentapikey = value; break; @@ -493,31 +715,51 @@ message = new ResponseMessage(); message.Header = "INFORMATION"; message.Type = MessageTypesEnum.INFORMATION; - try { - switch (setting) + if (e.Value != null) { - case "duplicatenamesallowed": - ConfigurationHelper.SetAppSetting("DuplicateNamesAllowed", e.Value.ToString()); - message.Message = "Duplicate Names Allowed setting updated to " + e.Value.ToString().ToUpper() + "!"; - break; - case "connectivitycheckenabled": - ConfigurationHelper.SetAppSetting("ConnectivityCheckEnabled", e.Value.ToString()); - message.Message = "Connectivity Check Enabled setting updated to " + e.Value.ToString().ToUpper() + "!"; - // Update the tool connection setting to true, if the connecvity check is disabled. This will cause the stie to always assume there is connectivity and attempt external connections. - if (!(bool)e.Value) - { - CacheHelper.SetCacheObject("isconnected", true); - } - else - { - CacheHelper.InvalidateCacheObject("isconnected"); - } - break; + switch (setting) + { + case "duplicatenamesallowed": + ConfigurationHelper.SetAppSetting("DuplicateNamesAllowed", e.Value.ToString()); + message.Message = "Duplicate Names Allowed setting updated to " + e.Value.ToString().ToUpper() + "!"; + break; + case "connectivitycheckenabled": + ConfigurationHelper.SetAppSetting("ConnectivityCheckEnabled", e.Value.ToString()); + message.Message = "Connectivity Check Enabled setting updated to " + e.Value.ToString().ToUpper() + "!"; + // Update the tool connection setting to true, if the connecvity check is disabled. This will cause the stie to always assume there is connectivity and attempt external connections. + if (!(bool)e.Value) + { + CacheHelper.SetCacheObject("isconnected", true); + } + else + { + CacheHelper.InvalidateCacheObject("isconnected"); + } + if (!(bool)e.Value) + { + await ShowInformationModal("bg-danger", "ATTENTION", "

Disabling this setting may result in connectivity errors being logged to the Admin Log.

"); + } + + break; + case "resourcetypeeditingallowed": + ConfigurationHelper.SetAppSetting("ResourceTypeEditingAllowed", e.Value.ToString()); + message.Message = "Resource Type Editing Allowed setting updated to " + e.Value.ToString().ToUpper() + "!"; + if ((bool)e.Value) + { + await ShowInformationModal("bg-danger", "ATTENTION", "

Disabling this setting may result in name generation that will fail regex validation for the resource type.

Please use caution when editing Resource Type validation settings."); + } + break; + } + message.Type = MessageTypesEnum.SUCCESS; + message.Header = "SUCCESS"; + } + else + { + message.Message = "There was a problem updating the setting!"; + message.Header = "ERROR"; } - message.Type = MessageTypesEnum.SUCCESS; - message.Header = "SUCCESS"; } catch (Exception ex) { @@ -543,8 +785,11 @@ toastService.ShowError(message.Message); break; } - AdminLogService.PostItem(new AdminLogMessage() { Title = message.Type.ToString(), Message = message.Message }); + AdminLogService.PostItem(new AdminLogMessage() { Title = message.Type.ToString(), Message = message.Message, Source = currentuser }); } + + servicesData = await ServicesHelper.LoadServicesData(servicesData, admin); + state.SetNavReload(true); StateHasChanged(); } @@ -604,4 +849,34 @@ AdminLogService.PostItem(new AdminLogMessage() { Title = "ERROR", Message = ex.Message }); } } + + async void ShowAddModal(int id, string type, string title, string message, string parentcomponent = "") + { + var parameters = new ModalParameters(); + parameters.Add(nameof(AddModal.id), id); + parameters.Add(nameof(AddModal.type), type); + parameters.Add(nameof(AddModal.message), message); + parameters.Add(nameof(AddModal.title), title); + if (parentcomponent != null) + { + parameters.Add(nameof(AddModal.parentcomponent), parentcomponent); + } + parameters.Add(nameof(AddModal.servicesData), servicesData); + parameters.Add("theme", theme); + + var options = new ModalOptions() + { + HideCloseButton = true, + UseCustomLayout = true + }; + + var modal = Modal.Show(title, parameters, options); + var result = await modal.Result; + if (!result.Cancelled) + { + servicesData = await ServicesHelper.LoadServicesData(servicesData, admin); + state.SetNavReload(true); + StateHasChanged(); + } + } } \ No newline at end of file diff --git a/ready/AzNamingTool/Pages/AdminLog.razor b/ready/AzNamingTool/Pages/AdminLog.razor index 4c7867fc..abc293b5 100644 --- a/ready/AzNamingTool/Pages/AdminLog.razor +++ b/ready/AzNamingTool/Pages/AdminLog.razor @@ -31,7 +31,7 @@

-
This page displays a log of admin/configuration changes.
+
This page displays a log of Admin/Configuration changes.
@if (!dataLoaded) @@ -54,8 +54,8 @@
-
- @@ -77,7 +77,7 @@ -
@@ -97,6 +97,7 @@ Created On + Source Title Message @@ -110,6 +111,9 @@ @message.CreatedOn.ToString() + + @message.Source + @message.Title @@ -220,7 +224,7 @@ if (!string.IsNullOrEmpty(filterData)) { - if ((!message.Title.Contains(filterData, StringComparison.OrdinalIgnoreCase)) && (!message.Message.Contains(filterData, StringComparison.OrdinalIgnoreCase))) + if ((!message.Source.Contains(filterData, StringComparison.OrdinalIgnoreCase)) && (!message.Title.Contains(filterData, StringComparison.OrdinalIgnoreCase)) && (!message.Message.Contains(filterData, StringComparison.OrdinalIgnoreCase))) visible = false; } return visible; diff --git a/ready/AzNamingTool/Pages/Configuration.razor b/ready/AzNamingTool/Pages/Configuration.razor index 0123ec1d..c52f9084 100644 --- a/ready/AzNamingTool/Pages/Configuration.razor +++ b/ready/AzNamingTool/Pages/Configuration.razor @@ -66,8 +66,8 @@
}
-
- @@ -156,13 +156,13 @@
-
-
+
Current Components
@@ -249,14 +249,14 @@ @if (admin) {
-
-
-
+
Export the current Components Configuration
@@ -271,7 +271,7 @@
-
+
Reset the Components Configuration
@@ -319,13 +319,13 @@ {
-
-
+
Delimiter
@@ -375,14 +375,14 @@ @if (admin) {
-
-
-
+
Export the current Delimiters Configuration
@@ -397,7 +397,7 @@
-
+
Reset the Delimiters Configuration
@@ -415,7 +415,7 @@
-
+
Import Delimiters Configuration
@@ -446,7 +446,7 @@ {
-
@@ -456,8 +456,8 @@ @if ((servicesData.CustomComponents.Where(x => x.ParentComponent == GeneralHelper.NormalizeName(customComponent.Name, true)) != null) && (customComponent.Name != "ResourceInstance")) {
-
- @@ -465,7 +465,7 @@ {
-
+
Current Components
@@ -541,14 +541,14 @@ @if (admin) {
-
-
-
+
Export the current Custom Components Configuration
@@ -566,7 +566,7 @@
-
+
Import Custom Components Configuration
@@ -601,13 +601,13 @@ {
-
-
+
Current Environments
@@ -671,14 +671,14 @@ @if (admin) {
-
-
-
+
Export the current Environments Configuration
@@ -693,7 +693,7 @@
-
+
Reset the Environments Configuration
@@ -711,7 +711,7 @@
-
+
Import Environments Configuration
@@ -740,13 +740,13 @@ {
-
-
+
Current Functions
@@ -811,14 +811,14 @@ @if (admin) {
-
-
-
+
Export Function Configuration
@@ -833,7 +833,7 @@
-
+
Reset the Functions Configuration
@@ -851,7 +851,7 @@
-
+
Import Function Configuration
@@ -880,13 +880,13 @@ {
-
-
+
Current Locations
@@ -980,14 +980,14 @@
*@
-
-
-
+
Export the current Locations Configuration
@@ -1004,7 +1004,7 @@ @if (ConfigurationHelper.VerifyConnectivity()) {
-
+
Refresh the Resource Locations Configuration
@@ -1039,7 +1039,7 @@
}
-
+
Reset the Locations Configuration
@@ -1057,7 +1057,7 @@
-
+
Import Locations Configuration
@@ -1086,13 +1086,13 @@ {
-
-
+
Current Orgs
@@ -1156,14 +1156,14 @@ @if (admin) {
-
-
-
+
Export the current Orgs Configuration
@@ -1178,7 +1178,7 @@
-
+
Reset the Orgs Configuration
@@ -1196,7 +1196,7 @@
-
+
Import Orgs Configuration
@@ -1228,13 +1228,13 @@ {
-
-
+
Current Projects/Apps/Services
@@ -1298,14 +1298,14 @@ @if (admin) {
-
-
-
+
Export the current Projects/Apps/Services Configuration
@@ -1320,7 +1320,7 @@
-
+
Reset the Projects/Apps/Services Configuration
@@ -1338,7 +1338,7 @@
-
+
Import Projects/Apps/Services Configuration
@@ -1367,13 +1367,13 @@ {
-
-
+
Current Resource Types
@@ -1411,8 +1411,8 @@ + @bind="filterData" + @bind:event="oninput">
@@ -1509,14 +1509,14 @@ @if (admin) {
-
-
-
+
Export the current Resource Types Configuration
@@ -1533,7 +1533,7 @@ @if (ConfigurationHelper.VerifyConnectivity()) {
-
+
Refresh the Resource Types Configuration
@@ -1572,7 +1572,7 @@
}
-
+
Reset the Resource Types Configuration
@@ -1590,7 +1590,7 @@
-
+
Import Resource Types Configuration
@@ -1619,13 +1619,13 @@ {
-
-
+
Current Units/Depts
@@ -1690,14 +1690,14 @@ @if (admin) {
-
-
-
+
Export Units/Depts Configuration
@@ -1712,7 +1712,7 @@
-
+
Reset the Units/Depts Configuration
@@ -1730,7 +1730,7 @@
-
+
Import Units/Depts Configuration
@@ -1763,29 +1763,35 @@

General Configuration

-
-
+
Export the current Global Configuration

- Click Export to export the current Global Configuration. This includes all component, settings, and admin passwords/API keys. + Click Export to export the current Global Configuration. This includes all components, options, and tool settings.

- Include Admin Configuration? + @if (currentuser == "GlobalAdmin") + { + + + Include Security/Identity Provider Settings? + + }

-
+
Reset the Global Configuration
@@ -1803,7 +1809,7 @@
-
+
Import Global Configuration
@@ -1826,13 +1832,13 @@ @*
-
-
+
Export Azure Policy Definition
@@ -1904,6 +1910,7 @@ private bool refreshtypeshortnames = false; private bool refreshlocationshortnames = false; private bool includeAdmin = false; + private string currentuser = String.Empty; private ResponseMessage message = new(); @@ -1913,6 +1920,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { + currentuser = await IdentityHelper.GetCurrentUser(session); if (firstRender) { var result = await session.GetAsync("admin"); @@ -2011,6 +2019,24 @@ { PropertyNameCaseInsensitive = true }; + + // Get the matching resource component for the item being configured + ResourceComponent thisresourceComponent = new(); + if (type != "ResourceComponent") + { + // Check if it's a custom component + if (type == "CustomComponent") + { + if (!String.IsNullOrEmpty(parentcomponent)) + { + thisresourceComponent = servicesData.ResourceComponents.Find(x => GeneralHelper.NormalizeName(x.Name, true) == GeneralHelper.NormalizeName(parentcomponent, true)); + } + } + else + { + thisresourceComponent = servicesData.ResourceComponents.Find(x => x.Name == type); + } + } switch (type) { case "ResourceComponent": @@ -2290,7 +2316,7 @@ } break; case "edit": - ShowEditModal((int)id, "ResourceEnvironment", "Edit Environment", "

Edit the Environment.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be 1-5 characters.
"); + ShowEditModal((int)id, "ResourceEnvironment", "Edit Environment", "

Edit the Environment.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be " + thisresourceComponent.MinLength + "-" + thisresourceComponent.MaxLength + " characters.
"); break; case "delete": confirm = await JsRuntime.InvokeAsync("confirm", "Are you sure?"); @@ -2312,7 +2338,7 @@ } break; case "add": - ShowAddModal((int)id, "ResourceEnvironment", "Add Environment", "

Add an Environment.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be 1-5 characters.
"); + ShowAddModal((int)id, "ResourceEnvironment", "Add Environment", "

Add an Environment.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be " + thisresourceComponent.MinLength + "-" + thisresourceComponent.MaxLength + " characters.
"); break; case "download": serviceResponse = await ResourceEnvironmentService.GetItems(); @@ -2405,7 +2431,7 @@ } break; case "edit": - ShowEditModal((int)id, "ResourceFunction", "Edit Function", "

Edit the Function.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be 1-10 characters.
"); + ShowEditModal((int)id, "ResourceFunction", "Edit Function", "

Edit the Function.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be " + thisresourceComponent.MinLength + "-" + thisresourceComponent.MaxLength + " characters.
"); break; case "delete": confirm = await JsRuntime.InvokeAsync("confirm", "Are you sure?"); @@ -2427,7 +2453,7 @@ } break; case "add": - ShowAddModal((int)id, "ResourceFunction", "Add Function", "

Add a Function.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be 1-10 characters.
"); + ShowAddModal((int)id, "ResourceFunction", "Add Function", "

Add a Function.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be " + thisresourceComponent.MinLength + "-" + thisresourceComponent.MaxLength + " characters.
"); break; case "download": serviceResponse = await ResourceFunctionService.GetItems(); @@ -2513,7 +2539,7 @@ } break; case "edit": - ShowEditModal((int)id, "ResourceLocation", "Edit Location", "

Edit the Location.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be 1-10 characters.
", true); + ShowEditModal((int)id, "ResourceLocation", "Edit Location", "

Edit the Location.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be " + thisresourceComponent.MinLength + "-" + thisresourceComponent.MaxLength + " characters.
", true); break; case "delete": confirm = await JsRuntime.InvokeAsync("confirm", "Are you sure?"); @@ -2643,7 +2669,7 @@ } break; case "edit": - ShowEditModal((int)id, "ResourceOrg", "Edit Org", "

Edit the Org.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be 1-5 characters.
"); + ShowEditModal((int)id, "ResourceOrg", "Edit Org", "

Edit the Org.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be " + thisresourceComponent.MinLength + "-" + thisresourceComponent.MaxLength + " characters.
"); break; case "delete": confirm = await JsRuntime.InvokeAsync("confirm", "Are you sure?"); @@ -2665,7 +2691,7 @@ } break; case "add": - ShowAddModal((int)id, "ResourceOrg", "Add Org", "

Add an Org.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be 1-5 characters.
"); + ShowAddModal((int)id, "ResourceOrg", "Add Org", "

Add an Org.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be " + thisresourceComponent.MinLength + "-" + thisresourceComponent.MaxLength + " characters.
"); break; case "download": serviceResponse = await ResourceOrgService.GetItems(); @@ -2758,7 +2784,7 @@ } break; case "edit": - ShowEditModal((int)id, "ResourceProjAppSvc", "Edit Project/App/Service", "

Edit the Project/App/Service.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be 1-3 characters.
"); + ShowEditModal((int)id, "ResourceProjAppSvc", "Edit Project/App/Service", "

Edit the Project/App/Service.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be " + thisresourceComponent.MinLength + "-" + thisresourceComponent.MaxLength + " characters.
"); break; case "delete": confirm = await JsRuntime.InvokeAsync("confirm", "Are you sure?"); @@ -2780,7 +2806,7 @@ } break; case "add": - ShowAddModal((int)id, "ResourceProjAppSvc", "Add Project/App/Service", "

Add an Project/App/Service.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be 1-3 characters.
"); + ShowAddModal((int)id, "ResourceProjAppSvc", "Add Project/App/Service", "

Add an Project/App/Service.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be " + thisresourceComponent.MinLength + "-" + thisresourceComponent.MaxLength + " characters.
"); break; case "download": serviceResponse = await ResourceProjAppSvcService.GetItems(); @@ -2866,7 +2892,8 @@ } break; case "edit": - ShowEditModal((int)id, "ResourceType", "Edit Resource Type", "

Edit the Resource Type.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be 1-10 characters.
", true); + resourceType = servicesData.ResourceTypes.Find(x => x.Id == id); + ShowEditModal((int)id, "ResourceType", "Edit Resource Type", "

Edit the Resource Type.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be " + resourceType.LengthMin + "-" + resourceType.LengthMax + " characters.
", true); break; case "download": serviceResponse = await ResourceTypeService.GetItems(); @@ -2977,7 +3004,7 @@ } break; case "edit": - ShowEditModal((int)id, "ResourceUnitDept", "Edit Unit/Dept", "

Edit the Unit/Dept.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be 1-3 characters.
"); + ShowEditModal((int)id, "ResourceUnitDept", "Edit Unit/Dept", "

Edit the Unit/Dept.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be " + thisresourceComponent.MinLength + "-" + thisresourceComponent.MaxLength + " characters.
"); break; case "delete": confirm = await JsRuntime.InvokeAsync("confirm", "Are you sure?"); @@ -2999,7 +3026,7 @@ } break; case "add": - ShowAddModal((int)id, "ResourceUnitDept", "Add Unit/Dept", "

Add a Unit/Dept.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be 1-3 characters.
"); + ShowAddModal((int)id, "ResourceUnitDept", "Add Unit/Dept", "

Add a Unit/Dept.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be " + thisresourceComponent.MinLength + "-" + thisresourceComponent.MaxLength + " characters.
"); break; case "download": serviceResponse = await ResourceUnitDeptService.GetItems(); @@ -3092,10 +3119,13 @@ } break; case "add": - ShowAddModal((int)id, "CustomComponent", "Add Custom Component", "

Add a Custom Component.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be 1-10 characters.
", parentcomponent); + // Get the component in the list + ShowAddModal((int)id, "CustomComponent", "Add Custom Component", "

Add a Custom Component.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be " + thisresourceComponent.MinLength + "-" + thisresourceComponent.MaxLength + " characters.
", parentcomponent); break; case "edit": - ShowEditModal((int)id, "CustomComponent", "Edit Custom Component", "

Edit the Custom Component.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be 1-10 characters.
"); + // Get the component in the list + customComponent = servicesData.CustomComponents.Find(x => x.Id == id); + ShowEditModal((int)id, "CustomComponent", "Edit Custom Component", "

Edit the Custom Component.

NOTES

  • Short Name value will be converted to lower case.
  • Short Name must be " + thisresourceComponent.MinLength + "-" + thisresourceComponent.MaxLength + " characters.
", false, customComponent.ParentComponent); break; case "delete": confirm = await JsRuntime.InvokeAsync("confirm", "Are you sure?"); @@ -3340,8 +3370,7 @@ workingmodal.Close(); } - - if (message.Message != null) + if (!String.IsNullOrEmpty(message.Message)) { message.Message = "(" + type + ") " + message.Message; switch (message.Type) @@ -3359,7 +3388,10 @@ toastService.ShowError(message.Message); break; } - AdminLogService.PostItem(new AdminLogMessage() { Title = message.Type.ToString(), Message = message.Message }); + + // Get the current user, if any + string currentuser = await IdentityHelper.GetCurrentUser(session); + AdminLogService.PostItem(new AdminLogMessage() { Title = message.Type.ToString(), Message = message.Message, Source = currentuser }); } } @@ -3446,6 +3478,7 @@ parameters.Add(nameof(AddModal.type), type); parameters.Add(nameof(AddModal.message), message); parameters.Add(nameof(AddModal.title), title); + parameters.Add(nameof(AddModal.servicesData), servicesData); if (parentcomponent != null) { parameters.Add(nameof(AddModal.parentcomponent), parentcomponent); @@ -3469,7 +3502,7 @@ } - async void ShowEditModal(int id, string type, string title, string message, bool protectedname = false) + async void ShowEditModal(int id, string type, string title, string message, bool protectedname = false, string? parentcomponent = null) { var parameters = new ModalParameters(); parameters.Add(nameof(EditModal.id), id); @@ -3477,6 +3510,8 @@ parameters.Add(nameof(EditModal.message), message); parameters.Add(nameof(EditModal.title), title); parameters.Add(nameof(EditModal.protectedName), protectedname); + parameters.Add(nameof(EditModal.parentcomponent), parentcomponent); + parameters.Add(nameof(EditModal.servicesData), servicesData); parameters.Add("theme", theme); var options = new ModalOptions() diff --git a/ready/AzNamingTool/Pages/Generate.razor b/ready/AzNamingTool/Pages/Generate.razor index 5b0293c5..64f57200 100644 --- a/ready/AzNamingTool/Pages/Generate.razor +++ b/ready/AzNamingTool/Pages/Generate.razor @@ -7,6 +7,7 @@ @inject ILogger Logger @inject NavigationManager NavigationManager @inject StateContainer state +@inject ProtectedSessionStorage session @inject IJSRuntime JsRuntime @inject IToastService toastService @@ -26,7 +27,7 @@
This page generates a name for the selected resource type.
-
+
Instructions
@@ -130,8 +131,8 @@ }
-
- @@ -200,7 +201,7 @@ @if (componentsVisible == "collapse") {
- +
} @@ -408,7 +409,7 @@ } @*
-
+
Delimiter (Configured by site administrator)
@@ -500,6 +501,7 @@ private ResourceNameResponse resourceNameRequestResponse; private ResourceType currentResourceType; private List multiselectedResourceTypes = new(); + private string? currentuser = String.Empty; protected override async Task OnInitializedAsync() { @@ -512,6 +514,9 @@ { await SetServicesData(); } + + var currentuservalue = await session.GetAsync("currentuser"); + currentuser = (string)currentuservalue.Value; } private async Task SetServicesData() @@ -557,6 +562,8 @@ resourceNameRequest.ResourceProjAppSvc = selectedResourceProjAppSvc; resourceNameRequest.ResourceUnitDept = selectedResourceUnitDept; resourceNameRequest.CustomComponents = selectedCustomComponents; + string currentuser = await IdentityHelper.GetCurrentUser(session); + resourceNameRequest.CreatedBy = currentuser; sbNames.Append(""); sbNames.Append(""); diff --git a/ready/AzNamingTool/Pages/GeneratedNamesLog.razor b/ready/AzNamingTool/Pages/GeneratedNamesLog.razor index 61507cc6..1ad89900 100644 --- a/ready/AzNamingTool/Pages/GeneratedNamesLog.razor +++ b/ready/AzNamingTool/Pages/GeneratedNamesLog.razor @@ -71,8 +71,8 @@ }
-
- @@ -94,7 +94,7 @@ -
@@ -118,6 +118,7 @@
} + @@ -140,6 +141,9 @@ + @@ -304,7 +308,7 @@ if (!string.IsNullOrEmpty(filterData)) { - if ((!name.ResourceName.ToLower().Contains(filterData)) && (!name.ResourceTypeName.ToLower().Contains(filterData)) && (!string.Join(",", name.Components).ToLower().Contains(filterData))) + if ((!name.User.Contains(filterData, StringComparison.OrdinalIgnoreCase)) && (!name.ResourceName.Contains(filterData, StringComparison.OrdinalIgnoreCase)) && (!name.ResourceTypeName.Contains(filterData, StringComparison.OrdinalIgnoreCase)) && (!string.Join(",", name.Components).Contains(filterData, StringComparison.OrdinalIgnoreCase))) visible = false; } return visible; diff --git a/ready/AzNamingTool/Pages/Index.razor b/ready/AzNamingTool/Pages/Index.razor index c00846cd..65b841d7 100644 --- a/ready/AzNamingTool/Pages/Index.razor +++ b/ready/AzNamingTool/Pages/Index.razor @@ -15,26 +15,38 @@

Azure Naming Tool

-

Disclaimer

-

+ +

+

Disclaimer

This tool was designed using the best practices in the Azure Cloud Adoption Framework which adheres to a few rules and allows the complete customization of your naming convention. While most of the customizations can be made in your web browser, this tool also includes an API. +
+
+ For guidance on the API, review your swagger page at: @(NavigationManager.Uri)swagger/index.html +

+

Admin

+

+ The Admin page is used to configure the Azure Naming Tool. This page is only accessible by Admin users.

+

Instructions

- For guidance on the API, review your swagger page at: @(NavigationManager.Uri)swagger/index.html + The Instructions page provides documentation for the site pages. Each page of the site contains a documentation link (top right) that will open contextual instructions for the current page.

Configuration

The Configuration page provides all the data points that make up your naming convention. There are 8 standard components that may be used to name each Azure resource. The data within each of those components can be completely customized, except for the resource types. For the resource types, only the short name, optional, and excluded values may be updated. A delimiter may be selected to separate each component in your resource names. However, the delimiter will be included in a resource name if that character is allowed. The tool also supports the ability to define custom components. This allows administratos to create new components for the naming convention. Your configuration may be exported under the Global Configuration header. We recommend doing that after defining your naming convention as a backup. @*Lastly, use the Policy export feature to create an Azure Policy Definition. That can be applied to your Azure environment to enforce your naming convention.*@

-

References

+

Reference

- The References page provides an example of each Azure resource type using your defined naming convention. The example values do not include any excluded naming components. Optional components are always displayed and are identified below the example. Since unique names are only required at specific scopes, the examples provided are only generated for the scopes above the resource scope: resource group, resource group & region, region, global, subscription, and tenant. + The Reference page provides an example of each Azure resource type using your defined naming convention. The example values do not include any excluded naming components. Optional components are always displayed and are identified below the example. Since unique names are only required at specific scopes, the examples provided are only generated for the scopes above the resource scope: resource group, resource group & region, region, global, subscription, and tenant.

- -

Generator

+

Generate

+

+ The Generate page provides a drop-down menu to select an Azure resource type. Once a resource is selected, the naming component options are provided. Read-only components cannot be changed, like the value for a resource type. Optional components, if unselected, will be null and not shown in the output. Required components do not allow a null value and will require a selection from the menu. +

+

Generated Names Log

- The Generator page provides a drop-down menu to select an Azure resource type. Once a resource is selected, the naming component options are provided. Read-only components cannot be changed, like the value for a resource type. Optional components, if unselected, will be null and not shown in the output. Required components do not allow a null value and will require a selection from the menu. + The Generated Names Log contains a record of all names generated in the Azure Naming Tool. This includes the date, user, generated name, resource type, and components selected. This log can be exported to a .csv file.

+ + } +

+
+ Minimum Length +
+
+ +
+
+
+
+ Maximum Length +
+
+ +
+
} else { - + @if ((type == "ResourceType") && (!Convert.ToBoolean(config.ResourceTypeEditingAllowed))) + { +
+ +
+
+ Minimum Length +
+
+ +
+
+
+
+ Maximum Length +
+
+ +
+
+ } } + @if (type == "ResourceComponent") { } - else - { -
-
- Short Name -
-
- -
-
- } @if ((type == "ResourceType") && (resourceComponents != null)) { @if (bool.Parse(ConfigurationHelper.GetAppSetting("DevMode"))) {
-
+
Regex
@@ -141,8 +221,8 @@
}
-
- @@ -173,8 +253,8 @@
-
- @@ -229,6 +309,8 @@ [Parameter] public string message { get; set; } [Parameter] public string type { get; set; } [Parameter] public bool protectedName { get; set; } + [Parameter] public string? parentcomponent { get; set; } + [Parameter] public ServicesData servicesData { get; set; } private ServiceResponse serviceResponse = new(); @@ -236,77 +318,82 @@ private string? itemDisplayName; private string? itemShortName; private string? itemRegEx; + private string? itemMinLength; + private string? itemMaxLength; + private string? itemMinLengthOriginal; + private string? itemMaxLengthOriginal; private List resourceComponents; private string? optional; private string? exclude; private bool ischecked = false; + private string? currentuser; private ResponseMessage responseMessage = new(); + private SiteConfiguration config = ConfigurationHelper.GetConfigurationData(); protected override async void OnInitialized() { switch (type) { case "ResourceComponent": - serviceResponse = await ResourceComponentService.GetItem(id); - ResourceComponent resourceComponent = (ResourceComponent)serviceResponse.ResponseObject; + ResourceComponent resourceComponent = servicesData.ResourceComponents.Find(x => x.Id == id); itemName = resourceComponent.Name; itemDisplayName = resourceComponent.DisplayName; + itemMinLength = resourceComponent.MinLength; + itemMinLengthOriginal = itemMinLength; + itemMaxLength = resourceComponent.MaxLength; + itemMaxLengthOriginal = itemMaxLength; break; case "ResourceEnvironment": - serviceResponse = await ResourceEnvironmentService.GetItem(id); - ResourceEnvironment resourceEnvironment = (ResourceEnvironment)serviceResponse.ResponseObject; + ResourceEnvironment resourceEnvironment = servicesData.ResourceEnvironments.Find(x => x.Id == id); itemName = resourceEnvironment.Name; itemShortName = resourceEnvironment.ShortName; break; case "ResourceLocation": - serviceResponse = await ResourceLocationService.GetItem(id); - ResourceLocation resourceLocation = (ResourceLocation)serviceResponse.ResponseObject; + ResourceLocation resourceLocation = servicesData.ResourceLocations.Find(x => x.Id == id); itemName = resourceLocation.Name; itemShortName = resourceLocation.ShortName; break; case "ResourceOrg": - serviceResponse = await ResourceOrgService.GetItem(id); - ResourceOrg resourceOrg = (ResourceOrg)serviceResponse.ResponseObject; + ResourceOrg resourceOrg = servicesData.ResourceOrgs.Find(x => x.Id == id); itemName = resourceOrg.Name; itemShortName = resourceOrg.ShortName; break; case "ResourceProjAppSvc": - serviceResponse = await ResourceProjAppSvcService.GetItem(id); - ResourceProjAppSvc resourceProjAppSvc = (ResourceProjAppSvc)serviceResponse.ResponseObject; + ResourceProjAppSvc resourceProjAppSvc = servicesData.ResourceProjAppSvcs.Find(x => x.Id == id); itemName = resourceProjAppSvc.Name; itemShortName = resourceProjAppSvc.ShortName; break; case "ResourceType": serviceResponse = await ResourceTypeService.GetItem(id); - ResourceType resourceType = (ResourceType)serviceResponse.ResponseObject; + ResourceType resourceType = servicesData.ResourceTypes.Find(x => x.Id == id); itemName = resourceType.Resource; itemShortName = resourceType.ShortName; optional = resourceType.Optional; exclude = resourceType.Exclude; itemRegEx = resourceType.Regx; - serviceResponse = await ResourceComponentService.GetItems(true); - resourceComponents = (List)serviceResponse.ResponseObject; - resourceComponents = resourceComponents.OrderBy(x => x.Name).ToList(); + itemMinLength = resourceType.LengthMin; + itemMinLengthOriginal = itemMinLength; + itemMaxLength = resourceType.LengthMax; + itemMaxLengthOriginal = itemMaxLength; + resourceComponents = servicesData.ResourceComponents.OrderBy(x => x.Name).ToList(); break; case "ResourceUnitDept": - serviceResponse = await ResourceUnitDeptService.GetItem(id); - ResourceUnitDept resourceUnitDept = (ResourceUnitDept)serviceResponse.ResponseObject; + ResourceUnitDept resourceUnitDept = servicesData.ResourceUnitDepts.Find(x => x.Id == id); itemName = resourceUnitDept.Name; itemShortName = resourceUnitDept.ShortName; break; case "ResourceFunction": - serviceResponse = await ResourceFunctionService.GetItem(id); - ResourceFunction resourceFunction = (ResourceFunction)serviceResponse.ResponseObject; + ResourceFunction resourceFunction = servicesData.ResourceFunctions.Find(x => x.Id == id); itemName = resourceFunction.Name; itemShortName = resourceFunction.ShortName; break; case "CustomComponent": - serviceResponse = await CustomComponentService.GetItem(id); - CustomComponent customComponent = (CustomComponent)serviceResponse.ResponseObject; + CustomComponent customComponent = servicesData.CustomComponents.Find(x => x.Id == id); itemName = customComponent.Name; itemShortName = customComponent.ShortName; break; } + currentuser = await IdentityHelper.GetCurrentUser(session); } async void Save() @@ -314,28 +401,46 @@ try { bool valid = false; - if (type == "ResourceComponent") + + // Make sure the min/max length is valid + if (Convert.ToInt32(itemMaxLength) < Convert.ToInt32(itemMinLength)) { - if (itemName != null) + toastService.ShowError("The Maximum Length must equal/greater than the Minimum length!"); + } + else + { + if (type == "ResourceComponent") { - if (itemName != "") + if (itemName != null) { - valid = true; - serviceResponse = await ResourceComponentService.GetItem(id); - ResourceComponent resourceComponent = (ResourceComponent)serviceResponse.ResponseObject; + if (itemName != "") + { + valid = true; + // Check if the user modified the min/max lengths and warn them to update their options + if ((itemMinLength != itemMinLengthOriginal) || (itemMaxLength != itemMaxLengthOriginal)) + { + toastService.ShowInfo("You have modified the min/max length. You will need to verify the existing options meet the new length requirements!"); + } - // Update any references in the custom components configuration to be the new custom component type name - serviceResponse = await CustomComponentService.GetItemsByParentType(GeneralHelper.NormalizeName(resourceComponent.Name, true)); - var customcomponents = (List)serviceResponse.ResponseObject; - foreach (var customcomponent in customcomponents) + ResourceComponent resourceComponent = servicesData.ResourceComponents.Find(x => x.Id == id); + + // Update any references in the custom components configuration to be the new custom component type name + var customcomponents = (List)servicesData.CustomComponents.FindAll(x => GeneralHelper.NormalizeName(x.ParentComponent, true) == GeneralHelper.NormalizeName(resourceComponent.Name, true)); + foreach (var customcomponent in customcomponents) + { + customcomponent.ParentComponent = GeneralHelper.NormalizeName(itemName, true); + serviceResponse = await CustomComponentService.PostItem(customcomponent); + } + resourceComponent.Name = itemName; + resourceComponent.DisplayName = itemName; + resourceComponent.MinLength = itemMinLength; + resourceComponent.MaxLength = itemMaxLength; + serviceResponse = await ResourceComponentService.PostItem(resourceComponent); + } + else { - customcomponent.ParentComponent = GeneralHelper.NormalizeName(itemName, true); - serviceResponse = await CustomComponentService.PostItem(customcomponent); + toastService.ShowError("You must enter a name!"); } - - resourceComponent.Name = itemName; - resourceComponent.DisplayName = itemName; - serviceResponse = await ResourceComponentService.PostItem(resourceComponent); } else { @@ -344,131 +449,134 @@ } else { - toastService.ShowError("You must enter a name!"); - } - } - else - { - if ((itemName != "") && (itemShortName != "")) - { - if (ValidationHelper.ValidateShortName(itemShortName, type)) + if ((itemName != "") && (itemShortName != "")) { - valid = true; - switch (type) + if (await ValidationHelper.ValidateShortName(type, itemShortName, this.parentcomponent)) { - case "ResourceEnvironment": - serviceResponse = await ResourceEnvironmentService.GetItem(id); - ResourceEnvironment resourceEnvironment = (ResourceEnvironment)serviceResponse.ResponseObject; - resourceEnvironment.Name = itemName; - resourceEnvironment.ShortName = itemShortName; - serviceResponse = await ResourceEnvironmentService.PostItem(resourceEnvironment); - break; - case "ResourceLocation": - serviceResponse = await ResourceLocationService.GetItem(id); - ResourceLocation resourceLocation = (ResourceLocation)serviceResponse.ResponseObject; - resourceLocation.ShortName = itemShortName; - serviceResponse = await ResourceLocationService.PostItem(resourceLocation); - break; - case "ResourceOrg": - serviceResponse = await ResourceOrgService.GetItem(id); - ResourceOrg resourceOrg = (ResourceOrg)serviceResponse.ResponseObject; - resourceOrg.Name = itemName; - resourceOrg.ShortName = itemShortName; - serviceResponse = await ResourceOrgService.PostItem(resourceOrg); - break; - case "ResourceProjAppSvc": - serviceResponse = await ResourceProjAppSvcService.GetItem(id); - ResourceProjAppSvc resourceProjAppSvc = (ResourceProjAppSvc)serviceResponse.ResponseObject; - resourceProjAppSvc.Name = itemName; - resourceProjAppSvc.ShortName = itemShortName; - serviceResponse = await ResourceProjAppSvcService.PostItem(resourceProjAppSvc); - break; - case "ResourceType": - serviceResponse = await ResourceTypeService.GetItem(id); - ResourceType resourceType = (ResourceType)serviceResponse.ResponseObject; - resourceType.ShortName = itemShortName; - resourceType.Optional = ""; - resourceType.Exclude = ""; - if (bool.Parse(ConfigurationHelper.GetAppSetting("DevMode"))) - { - resourceType.Regx = itemRegEx; - } + valid = true; + switch (type) + { + case "ResourceEnvironment": + ResourceEnvironment resourceEnvironment = servicesData.ResourceEnvironments.Find(x => x.Id == id); + resourceEnvironment.Name = itemName; + resourceEnvironment.ShortName = itemShortName; + serviceResponse = await ResourceEnvironmentService.PostItem(resourceEnvironment); + break; + case "ResourceLocation": + ResourceLocation resourceLocation = servicesData.ResourceLocations.Find(x => x.Id == id); + resourceLocation.ShortName = itemShortName; + serviceResponse = await ResourceLocationService.PostItem(resourceLocation); + break; + case "ResourceOrg": + ResourceOrg resourceOrg = servicesData.ResourceOrgs.Find(x => x.Id == id); + resourceOrg.Name = itemName; + resourceOrg.ShortName = itemShortName; + serviceResponse = await ResourceOrgService.PostItem(resourceOrg); + break; + case "ResourceProjAppSvc": + ResourceProjAppSvc resourceProjAppSvc = servicesData.ResourceProjAppSvcs.Find(x => x.Id == id); + resourceProjAppSvc.Name = itemName; + resourceProjAppSvc.ShortName = itemShortName; + serviceResponse = await ResourceProjAppSvcService.PostItem(resourceProjAppSvc); + break; + case "ResourceType": + ResourceType resourceType = servicesData.ResourceTypes.Find(x => x.Id == id); + resourceType.ShortName = itemShortName; + resourceType.Optional = ""; + resourceType.Exclude = ""; + if (!String.IsNullOrEmpty(itemMinLength.ToString())) + { + if (ValidationHelper.CheckNumeric(itemMinLength.ToString())) + { + resourceType.LengthMin = itemMinLength.ToString(); + } + } + if (!String.IsNullOrEmpty(itemMaxLength.ToString())) + { + if (ValidationHelper.CheckNumeric(itemMaxLength.ToString())) + { + resourceType.LengthMax = itemMaxLength.ToString(); + } + } + if (bool.Parse(ConfigurationHelper.GetAppSetting("DevMode"))) + { + resourceType.Regx = itemRegEx; + } - // Update the Optional and Exclude components - foreach (ResourceComponent resourceComponent in resourceComponents) - { - if (resourceComponent.Name != "ResourceType") + // Update the Optional and Exclude components + foreach (ResourceComponent resourceComponent in resourceComponents) { - // Optional - ischecked = false; - ischecked = await JsRuntime.InvokeAsync("IsElementChecked", "optional-" + GeneralHelper.NormalizeName(resourceComponent.Name, true)); - if (ischecked) + if (resourceComponent.Name != "ResourceType") { - List currentoptional = new List(resourceType.Optional.Split(',')); - if (!currentoptional.Contains(resourceComponent.Name)) + // Optional + ischecked = false; + ischecked = await JsRuntime.InvokeAsync("IsElementChecked", "optional-" + GeneralHelper.NormalizeName(resourceComponent.Name, true)); + if (ischecked) { - currentoptional.Add(GeneralHelper.NormalizeName(resourceComponent.Name, true)); - resourceType.Optional = String.Join(",", currentoptional.ToArray()); + List currentoptional = new List(resourceType.Optional.Split(',')); + if (!currentoptional.Contains(resourceComponent.Name)) + { + currentoptional.Add(GeneralHelper.NormalizeName(resourceComponent.Name, true)); + resourceType.Optional = String.Join(",", currentoptional.ToArray()); + } } } - } - // Exclude - ischecked = false; - ischecked = await JsRuntime.InvokeAsync("IsElementChecked", "exclude-" + GeneralHelper.NormalizeName(resourceComponent.Name, true)); - if (ischecked) - { - List currentexclude = new List(resourceType.Exclude.Split(',')); - if (!currentexclude.Contains(resourceComponent.Name)) + // Exclude + ischecked = false; + ischecked = await JsRuntime.InvokeAsync("IsElementChecked", "exclude-" + GeneralHelper.NormalizeName(resourceComponent.Name, true)); + if (ischecked) { - currentexclude.Add(GeneralHelper.NormalizeName(resourceComponent.Name, true)); - resourceType.Exclude = String.Join(",", currentexclude.ToArray()); + List currentexclude = new List(resourceType.Exclude.Split(',')); + if (!currentexclude.Contains(resourceComponent.Name)) + { + currentexclude.Add(GeneralHelper.NormalizeName(resourceComponent.Name, true)); + resourceType.Exclude = String.Join(",", currentexclude.ToArray()); + } } } - } - serviceResponse = await ResourceTypeService.PostItem(resourceType); - break; - case "ResourceUnitDept": - serviceResponse = await ResourceUnitDeptService.GetItem(id); - ResourceUnitDept resourceUnitDept = (ResourceUnitDept)serviceResponse.ResponseObject; - resourceUnitDept.Name = itemName; - resourceUnitDept.ShortName = itemShortName; - serviceResponse = await ResourceUnitDeptService.PostItem(resourceUnitDept); - break; - case "ResourceFunction": - serviceResponse = await ResourceFunctionService.GetItem(id); - ResourceFunction resourceFunction = (ResourceFunction)serviceResponse.ResponseObject; - resourceFunction.Name = itemName; - resourceFunction.ShortName = itemShortName; - serviceResponse = await ResourceFunctionService.PostItem(resourceFunction); - break; - case "CustomComponent": - serviceResponse = await CustomComponentService.GetItem(id); - CustomComponent customComponent = (CustomComponent)serviceResponse.ResponseObject; - customComponent.Name = itemName; - customComponent.ShortName = itemShortName; - serviceResponse = await CustomComponentService.PostItem(customComponent); - break; + serviceResponse = await ResourceTypeService.PostItem(resourceType); + break; + case "ResourceUnitDept": + ResourceUnitDept resourceUnitDept = servicesData.ResourceUnitDepts.Find(x => x.Id == id); + resourceUnitDept.Name = itemName; + resourceUnitDept.ShortName = itemShortName; + serviceResponse = await ResourceUnitDeptService.PostItem(resourceUnitDept); + break; + case "ResourceFunction": + ResourceFunction resourceFunction = servicesData.ResourceFunctions.Find(x => x.Id == id); + resourceFunction.Name = itemName; + resourceFunction.ShortName = itemShortName; + serviceResponse = await ResourceFunctionService.PostItem(resourceFunction); + break; + case "CustomComponent": + CustomComponent customComponent = servicesData.CustomComponents.Find(x => x.Id == id); + customComponent.Name = itemName; + customComponent.ShortName = itemShortName; + customComponent.MinLength = itemMinLength; + customComponent.MaxLength = itemMaxLength; + serviceResponse = await CustomComponentService.PostItem(customComponent); + break; + } + } + else + { + toastService.ShowError("You must enter a valid short name!"); } } else { - toastService.ShowError("You must enter a valid short name!"); + toastService.ShowError("You must enter a name and short name!"); } } - else - { - toastService.ShowError("You must enter a name and short name!"); - } } - if (valid) { if (serviceResponse.Success) { ModalInstance.CloseAsync(); toastService.ShowSuccess(GeneralHelper.NormalizeName(type, false) + " updated!"); - AdminLogService.PostItem(new AdminLogMessage() { Title = "SUCCESS", Message = GeneralHelper.NormalizeName(type, false) + " updated!" }); + AdminLogService.PostItem(new AdminLogMessage() { Title = "SUCCESS", Message = "(" + GeneralHelper.NormalizeName(type, false) + ") " + itemName + " updated!", Source = currentuser }); } else { @@ -505,11 +613,8 @@ switch (type) { case "ResourceComponent": - serviceResponse = await ResourceComponentService.GetItem(id); - ResourceComponent resourceComponent = (ResourceComponent)serviceResponse.ResponseObject; + ResourceComponent resourceComponent = servicesData.ResourceComponents.Find(x => x.Id == id); int componentid = (int)resourceComponent.Id; - serviceResponse = await ResourceTypeService.GetItems(); - List resourceTypes = (List)serviceResponse.ResponseObject; responseMessage.Message = resourceComponent.Name; List currentvalues = new(); switch (action) @@ -559,7 +664,7 @@ if (responseMessage.Message != null) { - responseMessage.Message = "(" + type + ") " + responseMessage.Message; + responseMessage.Message = "(" + GeneralHelper.NormalizeName(type, false) + ") " + itemName + " " + responseMessage.Message; switch (responseMessage.Type) { case MessageTypesEnum.INFORMATION: @@ -575,7 +680,7 @@ toastService.ShowError(responseMessage.Message); break; } - AdminLogService.PostItem(new AdminLogMessage() { Title = responseMessage.Type.ToString(), Message = responseMessage.Message }); + AdminLogService.PostItem(new AdminLogMessage() { Title = responseMessage.Type.ToString(), Message = responseMessage.Message, Source = currentuser }); } } } diff --git a/ready/AzNamingTool/Shared/MainLayout.razor b/ready/AzNamingTool/Shared/MainLayout.razor index 269dbf6d..3dc4619d 100644 --- a/ready/AzNamingTool/Shared/MainLayout.razor +++ b/ready/AzNamingTool/Shared/MainLayout.razor @@ -9,8 +9,10 @@ @inject NavigationManager NavigationManager @using AzureNamingTool.Services; @using Blazored.Toast.Configuration +@using System.Security.Claims; @inject ProtectedLocalStorage storage @inject ProtectedSessionStorage session +@inject IHttpContextAccessor httpContextAccessor Azure Naming Tool @@ -21,21 +23,36 @@
- @if ((admin) && (!String.IsNullOrEmpty(feedbackurl))) + @if (!String.IsNullOrEmpty(currentuser)) { -
- -
+ if (currentuser != "System") + { +
+ @( + "User: " + currentuser + ) +
+ } } - @if ((admin) && (bool.Parse(ConfigurationHelper.GetAppSetting("DevMode")))) + @if (admin) { -
DEV MODE Enabled! This can cause unexpected behavior! Use at your own risk!
+ @if (!String.IsNullOrEmpty(feedbackurl)) + { +
+ +
+ } } - @if (admin) + @if (!String.IsNullOrEmpty(currentuser)) { - + if (currentuser != "System") + { +
+ +
+ } } -
+
+ @if ((admin) && (bool.Parse(ConfigurationHelper.GetAppSetting("DevMode")))) + { +
DEV MODE Enabled! This can cause unexpected behavior! Use at your own risk!
+ } @Body @@ -59,12 +80,18 @@ } @code { + [CascadingParameter] + private IdentityProviderDetails? identityProviderDetails { get; set; } [CascadingParameter] public IModalService Modal { get; set; } - private ThemeInfo theme = new() { ThemeName = "Light", ThemeStyle = "bg-default text-black" }; + private ThemeInfo theme = new() { ThemeName = "Light", ThemeStyle = "bg-default text-dark" }; private bool isdarktheme = false; private bool admin; private string feedbackurl = String.Empty; + private string? currentuser = String.Empty; + + private string? details = String.Empty; + public bool PasswordModalOpen { get; set; } public Type PageType { get; set; } @@ -77,9 +104,6 @@ protected override async void OnInitialized() { - ConfigurationHelper.VerifyConfiguration(); - ConfigurationHelper.VerifySecurity(state); - // Check that the admin password is set if (!state.Password) { @@ -91,18 +115,59 @@ { var adminresult = await session.GetAsync("admin"); admin = (bool)adminresult.Value; + // Set the current user + var currentuservalue = await session.GetAsync("currentuser"); + if (!String.IsNullOrEmpty(currentuservalue.Value)) + { + currentuser = currentuservalue.Value; + } if (firstRender) { + // Check if the user has manually logged out. If so, don't attempt to identity them. + var logout = await session.GetAsync("logout"); + if (!(bool)logout.Value) + { + // Check if site is using an identity provider + if (!String.IsNullOrEmpty(identityProviderDetails.CurrentUser)) + { + if (identityProviderDetails.CurrentUser != "System") + { + // Show working modal + var workingmodalparameters = new ModalParameters(); + workingmodalparameters.Add("message", "Authentication sync in progress..."); + + var workingmodaloptions = new ModalOptions() + { + HideCloseButton = true, + UseCustomLayout = true + }; + + var workingmodal = Modal.Show("Working", workingmodalparameters, workingmodaloptions); + + // Get the current user value from the passed parameter (hosts.cshtml) + currentuser = identityProviderDetails.CurrentUser; + await session.SetAsync("currentuser", currentuser); + + // Log the user access + AdminLogService.PostItem(new AdminLogMessage() { Title = "INFORMATION", Message = "User accessed the site.", Source = currentuser }); + + // CHeck if the user is an admin + admin = await IdentityHelper.IsAdminUser(state, session, currentuser); + // Close modal + workingmodal.Close(); + } + } + } try { var themeresult = await storage.GetAsync("apptheme"); - theme.ThemeStyle = themeresult.Success ? themeresult.Value : "bg-default text-black"; + theme.ThemeStyle = themeresult.Success ? themeresult.Value : "bg-default text-dark"; } catch { - theme.ThemeStyle = "bg-default text-black"; + theme.ThemeStyle = "bg-default text-dark"; } - if (theme.ThemeStyle == "bg-default text-black") + if (theme.ThemeStyle == "bg-default text-dark") { theme.ThemeName = "Light"; isdarktheme = false; @@ -166,7 +231,7 @@ if (!isdarktheme) { theme.ThemeName = "Dark"; - theme.ThemeStyle = "bg-default text-black"; + theme.ThemeStyle = "bg-default text-dark"; } else { @@ -182,6 +247,8 @@ admin = false; state.SetAdmin(false); await session.SetAsync("admin", false); + await session.SetAsync("currentuser", "System"); + await session.SetAsync("logout", true); ResponseMessage message = new(); message.Type = MessageTypesEnum.INFORMATION; message.Header = "INFORMATION"; diff --git a/ready/AzNamingTool/Shared/MultiTypeSelectModal.razor b/ready/AzNamingTool/Shared/MultiTypeSelectModal.razor index 1be431ac..45f8977e 100644 --- a/ready/AzNamingTool/Shared/MultiTypeSelectModal.razor +++ b/ready/AzNamingTool/Shared/MultiTypeSelectModal.razor @@ -34,7 +34,7 @@
-
@@ -44,8 +44,8 @@ if (!IsCategoryVisible(category)) continue;
-
- diff --git a/ready/AzNamingTool/Shared/PasswordModal.razor b/ready/AzNamingTool/Shared/PasswordModal.razor index 0cf05acd..a4477965 100644 --- a/ready/AzNamingTool/Shared/PasswordModal.razor +++ b/ready/AzNamingTool/Shared/PasswordModal.razor @@ -12,11 +12,11 @@
Created OnCreated By Generated Name Resource Type Components @name.CreatedOn.ToString() + @name.User + @name.ResourceName ` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`