diff --git a/byos/waf/deploy/deploy.sh b/byos/waf/deploy/deploy.sh index f1a0ac0..73fb3e4 100644 --- a/byos/waf/deploy/deploy.sh +++ b/byos/waf/deploy/deploy.sh @@ -1,2 +1,74 @@ #!/bin/sh -dotnet run --project src/Deploy/Deploy.csproj -- "$@" \ No newline at end of file + +while getopts u:p:s:t:dvm flag +do + case "${flag}" in + u) username=${OPTARG};; + p) password=${OPTARG};; + s) subscription=${OPTARG};; + t) tenant=${OPTARG};; + d) deviceLogin=true;; + v) verbose=true;; + m) manualPat=true;; + esac +done + +if [ "$verbose" = true ] +then + set -x; +fi + +# Variables +rand="$((1 + $RANDOM % 100000000))" +rbac="rbacDeploy$rand" +devops="devopsDeploy$rand" + +cd /deploy + +echo "Cleaning authorization remnants..." +rm -f ./oh.azureauth + +echo "Logging into Azure via CLI and setting subscription ($subscription)..." +if [ "$deviceLogin" = true ] +then + az login --use-device-code --output none --allow-no-subscriptions +else + az login -u $username -p $password --output none --allow-no-subscriptions --only-show-errors +fi +az account set --subscription $subscription + +echo "Creating RBAC identity ($rbac) for deployment service principal..." +az ad sp create-for-rbac --only-show-errors --name $rbac --role Contributor --sdk-auth > oh.azureauth + +if [ "$manualPat" = false ] +then + echo "Creating managed app ($devops) for generating Azure DevOps PAT token..." + az ad app create --display-name $devops --native-app --required-resource-accesses @/source/azdo/manifest.json --output none + + # Wait 60 seconds to ensure that Azure has successfully created service princpals + sleep 60 + + appId=$(az ad app list --display-name $devops --query [0].appId) + appId="${appId%\"}" + appId="${appId#\"}" + az ad app permission admin-consent --id "${appId}" + + echo "Generating Azure AD access token to access Azure DevOps PAT API..." + access_token=$(curl -sS -X POST -d 'grant_type=password&client_id='$appId'&username='$username'&password='$password'&scope=499b84ac-1321-427f-aa17-267ca6975798/.default' https://login.microsoftonline.com/$tenant/oauth2/v2.0/token | jq '.access_token') + + echo "Deploying resources..." + ./Deploy -t $access_token -a oh.azureauth -s /source -i $subscription -o $rand +else + echo "Skipping: Creating managed app ($devops) for generating Azure DevOps PAT token..." + echo "Skipping: PAT token will be entered manually..." + + # Wait 60 seconds to ensure that Azure has successfully created service princpals + sleep 60 + + echo "Deploying resources..." + ./Deploy -a oh.azureauth -s /source -i $subscription -o $rand +fi + +echo +echo "Deployment completed." +echo \ No newline at end of file diff --git a/byos/waf/deploy/src/Deploy/AdoHelper.cs b/byos/waf/deploy/src/Deploy/AdoHelper.cs index b47ab8f..020afd9 100644 --- a/byos/waf/deploy/src/Deploy/AdoHelper.cs +++ b/byos/waf/deploy/src/Deploy/AdoHelper.cs @@ -63,7 +63,7 @@ public TeamProject CreateProject(string project) OperationReference operation = projectClient.QueueCreateProject(projectCreateParams).Result; - Operation completedOperation = WaitForLongRunningOperation(_connection, operation.Id, 5, 30).Result; + Operation completedOperation = WaitForLongRunningOperation(_connection, operation.Id, 5, 60).Result; if (completedOperation.Status == OperationStatus.Succeeded) { @@ -141,6 +141,8 @@ public void CommitRepository(string org, TeamProject project, GitRepository repo Console.WriteLine($"- Pushing repo '{repo.Name}'..."); diag.Process.Start("git", $@"init {Path.GetFullPath(path)}").WaitForExit(); + diag.Process.Start("git", $@"-C {Path.GetFullPath(path)} config user.email ""{org}@localhost.com""").WaitForExit(); + diag.Process.Start("git", $@"-C {Path.GetFullPath(path)} config user.name ""{org}""").WaitForExit(); diag.Process.Start("git", $@"-C {Path.GetFullPath(path)} branch -m main").WaitForExit(); diag.Process.Start("git", $@"-C {Path.GetFullPath(path)} remote add {org} {repo.RemoteUrl}").WaitForExit(); diag.Process.Start("git", $@"-C {Path.GetFullPath(path)} add .").WaitForExit(); diff --git a/byos/waf/deploy/src/Deploy/AzureHelper.cs b/byos/waf/deploy/src/Deploy/AzureHelper.cs index be31d5d..364914f 100644 --- a/byos/waf/deploy/src/Deploy/AzureHelper.cs +++ b/byos/waf/deploy/src/Deploy/AzureHelper.cs @@ -5,37 +5,46 @@ using Microsoft.Azure.Management.Fluent; using Microsoft.Azure.Management.ResourceManager.Fluent.Core; using Microsoft.Azure.Management.ResourceManager.Fluent.Models; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using System.Text; namespace Deploy { public class AzureHelper { private IAzure _azure; + private string _subscriptionId; + private string _orgId; private AzureHelper() { } - public AzureHelper(IAzure azure) + public AzureHelper(IAzure azure, string subscriptionId, string orgId) { _azure = azure; + _subscriptionId = subscriptionId; + _orgId = orgId; } - public void DeployTemplate(string subscriptionId, string path) + public void DeployTemplate(string path) { Console.WriteLine("- Building ARM Template from Bicep..."); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - Process.Start("CMD.exe", $@"/C az bicep build --file " + Path.GetFullPath(path + "\\main.bicep")).WaitForExit(); + Process.Start("CMD.exe", $@"/C az bicep build --file " + Path.GetFullPath(path + "/main.bicep")).WaitForExit(); else - Process.Start("az", $@"bicep build --file " + Path.GetFullPath(path + "\\main.bicep")).WaitForExit(); + Process.Start("az", $@"bicep build --file " + Path.GetFullPath(path + "/main.bicep")).WaitForExit(); Console.WriteLine("- Parsing ARM Template..."); - var templateJson = GetArmTemplate(Path.GetFullPath(path + "\\main.json")); + var templateJson = GetArmTemplate(Path.GetFullPath(path + "/main.json")); Console.WriteLine("- Creating resource group 'webapp'..."); _azure.ResourceGroups.Define("webapp") .WithRegion(Region.USEast) .Create(); - Console.WriteLine($"- Deploying template to subscription '{subscriptionId}' (this may take 15-20 minutes)..."); + Console.WriteLine($"- Deploying template to subscription '{_subscriptionId}' (this may take 15-20 minutes)..."); _azure.Deployments.Define("woodgrove") .WithExistingResourceGroup("webapp") .WithTemplate(templateJson) @@ -43,27 +52,55 @@ public void DeployTemplate(string subscriptionId, string path) .WithMode(DeploymentMode.Complete) .Create(); - File.Delete(Path.GetFullPath(path + "\\main.json")); + File.Delete(Path.GetFullPath(path + "/main.json")); + } + + public void DeployAzDoTemplate(string path) + { + Console.WriteLine("- Parsing ARM Template..."); + var templateJson = GetAzDoArmTemplate(Path.GetFullPath(path + "/azuredevops.json")); + + Console.WriteLine($"- Creating Azure DevOps tenant 'WAFOpenHack{_orgId}'..."); + _azure.ResourceGroups.Define("azdoTenant") + .WithRegion(Region.USCentral) + .Create(); + + Console.WriteLine($"- Deploying template to subscription '{_subscriptionId}' (this may take a few minutes)..."); + + Process.Start("az", $@"deployment group create --output none --resource-group azdoTenant --name azdo --template-file {System.IO.Path.GetFullPath(path + "/azuredevops.json")} --parameters devOpsOrgId={_orgId}").WaitForExit(); + + /* + * The below is commented out due to deploying an Azure DevOps tenant using the FluidAPI will generate an error about no user provided. + * Therefore, the above process is leveraging the Azure CLI for deploying Azure DevOps. Once the bug is fixed, the below code can be used + * in favor over the above process. + */ + /* + _azure.Deployments.Define("azdo") + .WithExistingResourceGroup("azdoTenant") + .WithTemplate(templateJson) + .WithParameters("{}") + .WithMode(DeploymentMode.Complete) + .Create(); + */ } public string GetArmTemplate(string templateFileName) { var armTemplateString = File.ReadAllText(templateFileName); var parsedTemplate = JObject.Parse(armTemplateString); - var rand = new Random().Next(0, 1000000).ToString("D6"); parsedTemplate.SelectToken("parameters.resource_group_name")["defaultValue"] = "webapp"; parsedTemplate.SelectToken("parameters.region")["defaultValue"] = "eastus"; parsedTemplate.SelectToken("parameters.vnet_name")["defaultValue"] = "vnet-webapp"; parsedTemplate.SelectToken("parameters.elb_name")["defaultValue"] = "elbwebapp"; parsedTemplate.SelectToken("parameters.nsg_name")["defaultValue"] = "nsg-webapp"; - parsedTemplate.SelectToken("parameters.storage_web")["defaultValue"] = "storwoodgroveweb" + rand; - parsedTemplate.SelectToken("parameters.storage_sql")["defaultValue"] = "storwoodgrovesql" + rand; - parsedTemplate.SelectToken("parameters.web1vm_dnslabel")["defaultValue"] = "woodgroveweb1" + rand; - parsedTemplate.SelectToken("parameters.web2vm_dnslabel")["defaultValue"] = "woodgroveweb2" + rand; - parsedTemplate.SelectToken("parameters.worker1vm_dnslabel")["defaultValue"] = "woodgroveworker1" + rand; - parsedTemplate.SelectToken("parameters.sqlsvr1vm_dnslabel")["defaultValue"] = "woodgrovesql1" + rand; - parsedTemplate.SelectToken("parameters.external_load_balancer_dnslabel")["defaultValue"] = "woodgroveelb" + rand; + parsedTemplate.SelectToken("parameters.storage_web")["defaultValue"] = "storwoodgroveweb" + _orgId; + parsedTemplate.SelectToken("parameters.storage_sql")["defaultValue"] = "storwoodgrovesql" + _orgId; + parsedTemplate.SelectToken("parameters.web1vm_dnslabel")["defaultValue"] = "woodgroveweb1" + _orgId; + parsedTemplate.SelectToken("parameters.web2vm_dnslabel")["defaultValue"] = "woodgroveweb2" + _orgId; + parsedTemplate.SelectToken("parameters.worker1vm_dnslabel")["defaultValue"] = "woodgroveworker1" + _orgId; + parsedTemplate.SelectToken("parameters.sqlsvr1vm_dnslabel")["defaultValue"] = "woodgrovesql1" + _orgId; + parsedTemplate.SelectToken("parameters.external_load_balancer_dnslabel")["defaultValue"] = "woodgroveelb" + _orgId; parsedTemplate.SelectToken("parameters.admin_username")["defaultValue"] = "cloudadmin"; parsedTemplate.SelectToken("parameters.admin_password")["defaultValue"] = "Pass@word1234!"; parsedTemplate.SelectToken("parameters.sql_admin_username")["defaultValue"] = "cloudsqladmin"; @@ -71,5 +108,35 @@ public string GetArmTemplate(string templateFileName) return parsedTemplate.ToString(); } + + public string GetAzDoArmTemplate(string templateFileName) + { + var armTemplateString = File.ReadAllText(templateFileName); + var parsedTemplate = JObject.Parse(armTemplateString); + + parsedTemplate.SelectToken("parameters.devOpsOrgId")["defaultValue"] = _orgId; + + return parsedTemplate.ToString(); + } + + public async Task GeneratePat(string accessToken) + { + using (var client = new HttpClient()) { + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + string json = Newtonsoft.Json.JsonConvert.SerializeObject(new { + displayName = "openhack", + scope = "app_token", + allOrgs = false + }); + + var data = new StringContent(json, Encoding.UTF8, "application/json"); + var response = await client.PostAsync($"https://vssps.dev.azure.com/WAFOpenHack{_orgId}/_apis/Tokens/Pats?api-version=6.1-preview", data); + + var result = response.Content.ReadAsStringAsync().Result; + var jObj = JObject.Parse(result); + return jObj["patToken"]["token"].ToString(); + } + } } } \ No newline at end of file diff --git a/byos/waf/deploy/src/Deploy/Options.cs b/byos/waf/deploy/src/Deploy/Options.cs index bd531a9..ceb822a 100644 --- a/byos/waf/deploy/src/Deploy/Options.cs +++ b/byos/waf/deploy/src/Deploy/Options.cs @@ -4,20 +4,20 @@ namespace Deploy { public class Options { - [Option('p', "pat", HelpText = "A personal access token for Azure DevOps.")] + [Option('t', "token", HelpText = "A generated Azure AD access token (NOTE: This is NOT an Azure DevOps PAT).")] public string AccessToken { get; set; } [Option('a', "auth", HelpText = "The path to the Azure AUTH file.")] public string AuthFile { get; set; } - [Option('o', "org", HelpText = "The name of the Azure DevOps organization.")] - public string Organization { get; set; } - [Option('s', "source", HelpText = "The path of the parent source folder.")] public string Source { get; set; } - [Option('u', "subscriptionId", HelpText = "The Azure subscription Id.")] + [Option('i', "id", HelpText = "The Azure subscription Id.")] public string SubscriptionId { get; set; } + + [Option('o', "org", HelpText = "Optional. A pre-generated organization id.")] + public string OrganizationId { get; set; } } } \ No newline at end of file diff --git a/byos/waf/deploy/src/Deploy/Program.cs b/byos/waf/deploy/src/Deploy/Program.cs index d80c9fc..c838ef6 100644 --- a/byos/waf/deploy/src/Deploy/Program.cs +++ b/byos/waf/deploy/src/Deploy/Program.cs @@ -12,33 +12,18 @@ class Program { static void Main(string[] args) { - VssCredentials creds = null; string accessToken = string.Empty; string path = string.Empty; string authFile = string.Empty; string subscriptionId = string.Empty; - string organization = string.Empty; + string organizationId = GenerateOrgId(); Parser.Default.ParseArguments(args) .WithParsed(o => { - if (String.IsNullOrWhiteSpace(o.AccessToken)) + if (!String.IsNullOrWhiteSpace(o.AccessToken)) { - throw new ArgumentNullException(@"Argument ""access token"" was not supplied."); - } - else - { - accessToken = o.AccessToken; - creds = new VssBasicCredential(string.Empty, o.AccessToken); - } - - if (String.IsNullOrWhiteSpace(o.Organization)) - { - throw new ArgumentNullException(@"Argument ""organization"" was not supplied."); - } - else - { - organization = o.Organization; + accessToken = o.AccessToken.Replace("\"", ""); } if (String.IsNullOrWhiteSpace(o.Source)) @@ -75,57 +60,110 @@ static void Main(string[] args) { subscriptionId = o.SubscriptionId; } + + if (!String.IsNullOrWhiteSpace(o.OrganizationId)) + { + organizationId = o.OrganizationId; + } }); - DeployDevOps(creds, accessToken, organization, path); - DeployAzureTenant(authFile, subscriptionId, path); + var azure = GenerateAzureInstance(authFile, subscriptionId); + DeployAzureDevOpsTenant(azure, subscriptionId, path, organizationId); + var pat = GeneratePat(organizationId, accessToken); + + DeployAzureTenant(azure, subscriptionId, path + "/repos", organizationId); + DeployDevOps(pat, path + "/repos", organizationId); } - static void DeployDevOps(VssCredentials credentials, string accessToken, string organization, string path) { - var adoHelper = new AdoHelper(credentials, "https://dev.azure.com/" + organization); + static string GenerateOrgId() + { + return new Random().Next(0, 1000000).ToString("D6"); + } + static IAzure GenerateAzureInstance(string authFile, string subscriptionId) + { + var credentials = SdkContext.AzureCredentialsFactory.FromFile(authFile); + + return Azure + .Configure() + .WithLogLevel(HttpLoggingDelegatingHandler.Level.Basic) + .Authenticate(credentials) + .WithSubscription(subscriptionId); + } + + static string GeneratePat(string orgId, string accessToken) + { + if (string.IsNullOrEmpty(accessToken)) + { + Console.WriteLine(); + Console.Write($"Token not provided. Enter PAT manually (WAFOpenHack{orgId}): "); + accessToken = Console.ReadLine(); + + return accessToken; + } + else + { + var azureHelper = new AzureHelper(null, null, orgId); + + return azureHelper.GeneratePat(accessToken).Result; + } + } + + static void DeployAzureDevOpsTenant(IAzure azure, string subscriptionId, string path, string orgId) + { Console.WriteLine("*********************************"); Console.WriteLine("* *"); Console.WriteLine("* Deploying Azure DevOps Tenant *"); Console.WriteLine("* *"); Console.WriteLine("*********************************"); - // Create Bicep project - var bicepProject = adoHelper.CreateProject("Bicep"); - var bicepTempRepo = adoHelper.CreateRepository(bicepProject, "temp", true); - adoHelper.RemoveRepository(bicepProject, "Bicep", isDefault: true); - var bicepRepo = adoHelper.CreateRepository(bicepProject, "bicep"); - adoHelper.CommitRepository(organization, bicepProject, bicepRepo, accessToken, path + "\\bicep"); - adoHelper.RemoveRepository(bicepProject, "temp", isTemp: true); + var azureHelper = new AzureHelper(azure, subscriptionId, orgId); + azureHelper.DeployAzDoTemplate(path + "/azdo"); - // Create Portal project - var portalProject = adoHelper.CreateProject("Portal"); - var processorRepo = adoHelper.CreateRepository(portalProject, "processor"); - adoHelper.CommitRepository(organization, portalProject, processorRepo, accessToken, path + "\\portal\\processor"); - var webRepo = adoHelper.CreateRepository(portalProject, "web"); - adoHelper.CommitRepository(organization, portalProject, webRepo, accessToken, path + "\\portal\\web"); - adoHelper.RemoveRepository(portalProject, "Portal", isDefault: true); + Console.WriteLine(); + } + + static void DeployAzureTenant(IAzure azure, string subscriptionId, string path, string orgId) + { + Console.WriteLine("*********************************"); + Console.WriteLine("* *"); + Console.WriteLine("* Deploying Azure Resources *"); + Console.WriteLine("* *"); + Console.WriteLine("*********************************"); + + var azureHelper = new AzureHelper(azure, subscriptionId, orgId); + azureHelper.DeployTemplate(path + "/bicep"); Console.WriteLine(); } - static void DeployAzureTenant(string authFile, string subscriptionId, string path) { - var credentials = SdkContext.AzureCredentialsFactory.FromFile(authFile); + static void DeployDevOps(string accessToken, string path, string orgId) + { + var credentials = new VssBasicCredential(string.Empty, accessToken); + var organization = "WAFOpenHack" + orgId; + var adoHelper = new AdoHelper(credentials, "https://dev.azure.com/" + organization); Console.WriteLine("*********************************"); Console.WriteLine("* *"); - Console.WriteLine("* Deploying Azure Resources *"); + Console.WriteLine("* Deploying Azure DevOps Repos *"); Console.WriteLine("* *"); Console.WriteLine("*********************************"); - var azure = Azure - .Configure() - .WithLogLevel(HttpLoggingDelegatingHandler.Level.Basic) - .Authenticate(credentials) - .WithSubscription(subscriptionId); + // Create Bicep project + var bicepProject = adoHelper.CreateProject("Bicep"); + var bicepTempRepo = adoHelper.CreateRepository(bicepProject, "temp", true); + adoHelper.RemoveRepository(bicepProject, "Bicep", isDefault: true); + var bicepRepo = adoHelper.CreateRepository(bicepProject, "bicep"); + adoHelper.CommitRepository(organization, bicepProject, bicepRepo, accessToken, path + "/bicep"); + adoHelper.RemoveRepository(bicepProject, "temp", isTemp: true); - var azureHelper = new AzureHelper(azure); - azureHelper.DeployTemplate(subscriptionId, path + "\\bicep"); + // Create Portal project + var portalProject = adoHelper.CreateProject("Portal"); + var processorRepo = adoHelper.CreateRepository(portalProject, "processor"); + adoHelper.CommitRepository(organization, portalProject, processorRepo, accessToken, path + "/portal/processor"); + var webRepo = adoHelper.CreateRepository(portalProject, "web"); + adoHelper.CommitRepository(organization, portalProject, webRepo, accessToken, path + "/portal/web"); + adoHelper.RemoveRepository(portalProject, "Portal", isDefault: true); Console.WriteLine(); } diff --git a/byos/waf/deployment.md b/byos/waf/deployment.md index bb7f355..c7f6b7a 100644 --- a/byos/waf/deployment.md +++ b/byos/waf/deployment.md @@ -6,63 +6,118 @@ This deployment guide will assist you in deploying the required resources and ar ## Prerequisites -* Azure DevOps Organization/Tenant (the script will create the necessary projects and upload the artifacts) -* Azure subscription -* [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli) -* [Bicep Azure CLI extension](https://github.com/Azure/bicep/blob/main/docs/installing.md#install-the-bicep-cli-details) -* [.NET 5.0 Runtime](https://dotnet.microsoft.com/download/dotnet/5.0) -* [Git CLI](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +Because of the complexity of the deployment, it is _highly_ recommended to use Docker for deploying resources. While you can attempt to interpret the deployment shell script, it is not advised. Being as such, besides an Azure subscription and Azure DevOps account, **Docker is the _only_ prerequisite** and approximately 20GB of temporary space to build the container. + +It is also worth noting that, unless limits have been increased, typical Azure tenants may not have the necessary compute allocation to deploy the initial resources. ## Deployment -The deployment script **requires** the following five parameters. -| Flag | Description | Help | -| ---- | ----------- | ---- | -| `-p` | A personal access token for Azure DevOps | [Create a PAT](https://docs.microsoft.com/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page#create-a-pat) | -| `-a` | The path to the Azure AUTH file. | [Using an authentication file](https://github.com/Azure/azure-libraries-for-net/blob/master/AUTH.md#using-an-authentication-file) | -| `-o` | The name of the Azure DevOps organization. | | -| `-s` | The path of the parent source folder. The source folder is the parent folder containing the `bicep` and `portal` folders.| | -| `-u` | The Azure subscription Id. | | +Please fully read this section _prior to_ attempting a deployment. As stated above, Docker is recommended for deploying the OpenHack due to the deployment complexity. Therefore, all instructions will be based on this approach. Also, the Docker image is only required for _deployment_. Once the OpenHack has been deployed to your Azure and DevOps tenants, you can delete the local Docker image to reclaim space. + +Deployment can be accomplished two ways: fully-automated and semi-automated. Both approaches are shared in detail below. All available parameters are listed, as well. Finally, some examples are provided for different scenarios. + +>NOTE: During the deployment, generating the ARM template from the Bicep definition files will generate some warnings. This is expected and due to the Azure REST API not being updated. + +### Fully-automated deployment + +A fully-automated deployment **requires** that the one performing the deployment is a _Global Admin_ on the Azure tenant. This requirement is due to assigning the appropriate permissions (e.g. "admin consent") to the generated Azure AD app registrations. In a fully-automated deployment, the necessary app registrations are created automatically, are granted "admin consent," and a Personal Access Token (PAT) is automatically created in Azure DevOps. + +Unless you are the IT manager of your company or attempting to deploy the OpenHack artifacts to a _personal_ subscription, this approach will not be available to you and, therefore, you should follow the semi-automated deployment method. + +### Semi-automated deployment + +In a semi-automated deployment, only a single service principal is created with RBAC privileges in the Azure tenant. You will be required to create a PAT in Azure DevOps during the deployment process. + +A semi-automated deployment will be the necessary approach for most users, _including Microsoft employees_ wishing to deploy the OpenHack into their AIRS subscription. + +### Stages of deployment + +The deployment has four basic stages: + +1) Create the required Azure identities +2) Deploy an Azure DevOps tenant +3) Deploy Azure portal artifacts +4) Deploy Azure DevOps artifacts + +### Available parameters + +The Azure AD REST APIs, unfortunately, are inconsistent with the returned JWT tokens. Therefore, a few different endpoints are required for deployment. For this reason, you will notice below that your Azure AD username and password are always required regardless of using a username/password or device login. + +| Flag | Required | Description | +| :--: | :------: | ----------- | +| `-u` | **Yes** | Your Azure AD account email address. | +| `-p` | **Yes** | Your Azure AD account password.

NOTE: It may be helpful to enclose your password in single quotes if your password contains special characters. | +| `-t` | **Yes** | Your Azure tenant unique ID (e.g., a GUID). | +| `-s` | **Yes** | Your Azure subscription ID (e.g., a GUID). | | +| `-d` | | If your tenant requires multi-factor authentication (MFA), this flag is **required**. This will allow you to authenticate in a browser in order to run Azure CLI commands as MFA restricts username/password authentication via `az login`. | +| `-m` | | If you are **NOT** a Global Admin on your Azure tenant, this flag is **required**. Setting this flag will provide you the opportunity to _manually_ create and apply an Azure DevOps PAT during the deployment process. | +| `-v` | | Enables verbose logging. | + +### Manually generating a Personal Access Token (PAT) in Azure DevOps + +If you are executing a semi-automated deployment (setting the `-m` flag), you will be prompted to create and provide a PAT _after the second stage_ of the deployment completes the creation of your Azure DevOps tenant. + +The deployment script will inform you of the name of your newly created tenant (e.g. WAFOpenHack###### ). You will need to create a PAT with the two permissions listed below. Note that it is not important what name you assign to the PAT or its expiration date. The only requirements are that the PAT is assigned to the newly-created organization and setting the two permissions: + +1) **Code** - _Full_ +2) **Project and Team** - _Read, write, & manage_ + +> (NOTE: You will need to click on _"Show all scopes"_ at the bottom of the blade in order to find the **Project and Team** permission scope.) + +![PAT requirements](images/pat_requirements.png) -### Steps +If you need help creating a PAT, review the [documentation](https://docs.microsoft.com/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page). -1. If necessary, create a target Azure DevOps organization -2. Retrieve your Azure DevOps PAT (see above) -3. If necessary, create a target Azure subscription -4. Generate an Azure AUTH file (see above) -5. Run the command below +### Examples -Assuming you are currently in the `/deploy` folder: +#### Build the Docker image -#### bash +Prior to running any of the below examples, you will need to first build an image. In the project's root folder execute the following: ```bash -chmod 755 ./deploy.sh -./deploy.sh -p [pat] -o [organization] -s ../source -a [auth.file] -u [subscriptionId] +docker build -t wafopenhack ``` -#### command prompt +This will create a Docker container image called _wafopenhack_. You can actually choose whatever you'd like for the name of the container. However, the examples below assumes that you've chosen this name for your image. + +> NOTE: Creating an image for the first time may take a few minutes and is largely dependent on how fast resources (other image layers) can be downloaded and the deployment application compiling. + +#### Fully-automated deployment + +Again, in order to execute this deployment you must be a _Global Admin_ on your Azure tenant _and_ MFA must **NOT** be required. ```bash -deploy -p [pat] -o [organization] -s ..\source -a [auth.file] -u [subscriptionId] +docker run -it wafopenhack -u -p -s -t ``` ->NOTE: Generating the ARM template from the Bicep definition files will generate some warnings. This is expected and due to the Azure REST API not being updated. - -## Process - -The deployment application follows the below execution plan. - -1. Deploys the Azure DevOps artifacts - 1. Creates the `Bicep` project in Azure DevOps - 2. Creates a `bicep` repository - 3. Commits Bicep artifacts to repository - 4. Creates the 'Portal` project in Azure DevOps - 5. Creates a `processor` repository - 6. Commits Processor source files to repository - 7. Creates a `web` repository - 8. Commits Web (UI and API) source files to repository -2. Deploys the initial Azure resources - 1. Builds the ARM template from the Bicep definition - 2. Creates a resource group - 3. Deploys the ARM template to the Azure subscription +#### Using device login + +This approach is used for Azure AD tenants _requiring_ MFA authentication. + +```bash +docker run -it wafopenhack -u -p -s -t -d +``` + +Upon running this, you will be directed to a website where you will need to enter a code and authenticate. Upon successful authentication, the script will proceed. + +#### Using a manually-generated PAT + +This approach is used for Azure AD tenants _requiring_ MFA authentication. + +```bash +docker run -it wafopenhack -u -p -s -t -m +``` + +As stated above, after your Azure DevOps tenant has been created, you will prompted to manually enter a PAT. Follow the requirements stated above for creating a PAT with the necessary permissions. Once, you've created the PAT, enter it at the prompt (copy and paste), then the deployment will continue. + +#### Microsoft AIRS subscriptions (for Microsoft employees) + +As stated previously, because of security limitations within the Microsoft tenant, you must use a combination of the device login and manually-generated PAT. + +```bash +docker run -it wafopenhack -u -p -s -t -d -m +``` + +## Assistance + +This deployment script, the artifacts within the repo, and any other content contained herein are provided with little assistance. Should you have questions or have issues, please open an _Issue_ in this repository. diff --git a/byos/waf/source/dsc/Portal.Api.zip b/byos/waf/source/dsc/Portal.Api.zip index 016c6e4..74df0e0 100644 Binary files a/byos/waf/source/dsc/Portal.Api.zip and b/byos/waf/source/dsc/Portal.Api.zip differ diff --git a/byos/waf/source/dsc/Portal.Web.zip b/byos/waf/source/dsc/Portal.Web.zip index 22940c2..591992c 100644 Binary files a/byos/waf/source/dsc/Portal.Web.zip and b/byos/waf/source/dsc/Portal.Web.zip differ diff --git a/byos/waf/source/dsc/Processor.zip b/byos/waf/source/dsc/Processor.zip index 836d896..a729feb 100644 Binary files a/byos/waf/source/dsc/Processor.zip and b/byos/waf/source/dsc/Processor.zip differ