diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d59d22f67..bbadb528d 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,9 @@ Release Notes ============= +## 1.8.7 +* B2C Tenants: Support for creating B2C Tenants. + ## 1.8.6 * Route Server: Custom name support for Route Server Public IP. diff --git a/docs/content/api-overview/resources/b2c-tenant.md b/docs/content/api-overview/resources/b2c-tenant.md new file mode 100644 index 000000000..f1b158731 --- /dev/null +++ b/docs/content/api-overview/resources/b2c-tenant.md @@ -0,0 +1,60 @@ +--- +title: "B2C Tenant" +date: 2024-02-01T00:00:00+01:00 +chapter: false +weight: 1 +--- + +#### Overview +Creates a new B2C tenant, please note that the current implementation only supports the creation of a new B2C tenant. + +Usage of this computation expression when a B2C tenant already exists will result in an error, check the [example](#example) for more infos. + +#### B2C Tenant Builder Keywords +| Applies To | Keyword | Purpose | +|-|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| B2C Tenant | initial_domain_name | Initial domain name for the B2C tenant as in `initial_domain_name.onmicrosoft.com` | +| B2C Tenant | display_name | Display name for the B2C tenant. | +| B2C Tenant | sku | [SKU](https://learn.microsoft.com/en-us/rest/api/activedirectory/b2c-tenants/list-by-subscription?view=rest-activedirectory-2021-04-01&tabs=HTTP#b2cresourceskuname) for the B2C tenant. | +| B2C Tenant | country_code | Country code defined by two capital letter, for examples check the official [docs](https://learn.microsoft.com/en-us/azure/active-directory-b2c/data-residency] | +| B2C Tenant | data_residency | Data residency for the B2C tenant, for more infos check the official [docs](https://learn.microsoft.com/en-us/azure/active-directory-b2c/data-residency] | +| B2C Tenant | tags | Tags for the B2C tenant. | + +#### Example + +Basic creation of a B2C tenant, while avoiding having an error when such tenant already exists. + +```fsharp +open Farmer +open Farmer.B2cTenant +open Farmer.Builders +open Farmer.Deploy + +let initialDomainName = "myb2c" + +let myb2c = + b2cTenant { + initial_domain_name initialDomainName + display_name "My B2C" + sku Sku.PremiumP1 + country_code "FR" + data_residency B2cDataResidency.Europe + } + +let b2cDoesNotExist (initialDomainName: string) = + let output = + Az.AzHelpers.executeAz $"resource list --name '{initialDomainName}.onmicrosoft.com'" + |> snd + not (output.Contains initialDomainName) + +let deployment = + arm { + location Location.FranceCentral + add_resources + [ + // This allows to avoid having an error when the B2C tenant already exists + if b2cDoesNotExist initialDomainName then + myb2c + ] + } +```` diff --git a/src/Farmer/Arm/B2cTenant.fs b/src/Farmer/Arm/B2cTenant.fs new file mode 100644 index 000000000..6836154b5 --- /dev/null +++ b/src/Farmer/Arm/B2cTenant.fs @@ -0,0 +1,46 @@ +[] +module Farmer.Arm.B2cTenant + +open Farmer + +let b2cTenant = + ResourceType("Microsoft.AzureActiveDirectory/b2cDirectories", "2021-04-01") + +type B2cDomainName = + | B2cDomainName of string + + static member internal Empty = B2cDomainName "" + + member this.AsResourceName = + match this with + | B2cDomainName name -> ResourceName name + +type B2cTenant = + { + Name: B2cDomainName + DisplayName: string + DataResidency: Location + CountryCode: string + Tags: Map + Sku: B2cTenant.Sku + } + + interface IArmResource with + member this.ResourceId = accounts.resourceId this.Name.AsResourceName + + member this.JsonModel = + {| b2cTenant.Create(this.Name.AsResourceName, this.DataResidency, tags = this.Tags) with + sku = + {| + name = string this.Sku + tier = "A0" + |} + properties = + {| + createTenantProperties = + {| + countryCode = this.CountryCode + displayName = this.DisplayName + |} + |} + |} diff --git a/src/Farmer/Builders/Builders.B2cTenant.fs b/src/Farmer/Builders/Builders.B2cTenant.fs new file mode 100644 index 000000000..735186f6e --- /dev/null +++ b/src/Farmer/Builders/Builders.B2cTenant.fs @@ -0,0 +1,83 @@ +[] +module Farmer.Builders.B2cTenant + +open Farmer +open Farmer.Arm +open Farmer.Validation +open Farmer.B2cTenant + +type B2cDomainName with + + static member FromInitialDomainName initialDomainName = + [ containsOnlyM [ lettersOrNumbers ] ] + |> validate "B2c initial domain name" initialDomainName + |> Result.map (fun x -> B2cDomainName $"{x}.onmicrosoft.com") + +type B2cTenantConfig = + { + Name: B2cDomainName + DisplayName: string + DataResidency: Location + CountryCode: string + Sku: Sku + Tags: Map + } + + interface IBuilder with + member this.ResourceId = b2cTenant.resourceId this.Name.AsResourceName + + member this.BuildResources _ = + [ + { + B2cTenant.Name = this.Name + DisplayName = this.DisplayName + DataResidency = this.DataResidency + CountryCode = this.CountryCode + Tags = this.Tags + Sku = this.Sku + } + ] + +type B2cTenantBuilder() = + member _.Yield _ = + { + Name = B2cDomainName.Empty + DisplayName = "" + DataResidency = B2cDataResidency.Europe.Location + CountryCode = "FR" + Sku = Sku.PremiumP1 + Tags = Map.empty + } + + [] + member _.InitialDomainName(state: B2cTenantConfig, name: string) = + { state with + Name = B2cDomainName.FromInitialDomainName(name).OkValue + } + + [] + member _.DisplayName(state: B2cTenantConfig, displayName: string) = + { state with DisplayName = displayName } + + [] + member _.Sku(state: B2cTenantConfig, sku: Sku) = { state with Sku = sku } + + /// Data residency location as described in: https://learn.microsoft.com/en-us/azure/active-directory-b2c/data-residency#data-residency + [] + member _.DataResidency(state: B2cTenantConfig, b2cDataResidency: B2cDataResidency) = + { state with + DataResidency = b2cDataResidency.Location + } + + /// Country Code defined by two capital letters (example: FR), as described in: https://learn.microsoft.com/en-us/azure/active-directory-b2c/data-residency#data-residency + [] + member _.CountryCode(state: B2cTenantConfig, countryCode: string) = + { state with CountryCode = countryCode } + + interface ITaggable with + member _.Add state tags = + { state with + Tags = state.Tags |> Map.merge tags + } + +let b2cTenant = B2cTenantBuilder() diff --git a/src/Farmer/Common.fs b/src/Farmer/Common.fs index 620122314..ed6d65a0a 100644 --- a/src/Farmer/Common.fs +++ b/src/Farmer/Common.fs @@ -2301,6 +2301,28 @@ module ContainerService = | Kubenet -> "kubenet" | AzureCni -> "azure" +module B2cTenant = + type Sku = + | PremiumP1 + | PremiumP2 + | Standard + + /// Check official documentation for more details: https://learn.microsoft.com/en-us/azure/active-directory-b2c/data-residency#data-residency + type B2cDataResidency = + | UnitedStates + | Europe + | AsiaPacific + | Japan + | Australia + + member this.Location = + match this with + | UnitedStates -> Location "United States" + | Europe -> Location "Europe" + | AsiaPacific -> Location "Asia Pacific" + | Japan -> Location "Japan" + | Australia -> Location "Australia" + module Redis = type Sku = | Basic diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index f1dc6b15c..f81a41581 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -121,10 +121,12 @@ + + diff --git a/src/Tests/AllTests.fs b/src/Tests/AllTests.fs index f78708e60..a8d1ca0aa 100644 --- a/src/Tests/AllTests.fs +++ b/src/Tests/AllTests.fs @@ -26,6 +26,7 @@ let allTests = AzCli.tests AutoscaleSettings.tests AzureFirewall.tests + B2cTenant.tests Bastion.tests BingSearch.tests Cdn.tests diff --git a/src/Tests/B2cTenant.fs b/src/Tests/B2cTenant.fs new file mode 100644 index 000000000..591358a1d --- /dev/null +++ b/src/Tests/B2cTenant.fs @@ -0,0 +1,70 @@ +module B2cTenant + +open Expecto +open Farmer +open Farmer.B2cTenant +open Farmer.Builders +open Newtonsoft.Json.Linq + +let tests = + testList + "B2c tenant tests" + [ + test "B2c tenant should generate the expected arm template" { + let deployment = + arm { + location Location.FranceCentral + + add_resources + [ + b2cTenant { + initial_domain_name "myb2c" + display_name "My B2C tenant" + sku Sku.PremiumP1 + country_code "FR" + data_residency B2cDataResidency.Europe + } + ] + } + + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + + let generatedTemplate = jobj.SelectToken("resources[0]") + + Expect.equal + (generatedTemplate.SelectToken("apiVersion").ToString()) + "2021-04-01" + "Invalid ARM template api version" + + Expect.equal + (generatedTemplate.SelectToken("type").ToString()) + "Microsoft.AzureActiveDirectory/b2cDirectories" + "Invalid ARM template type" + + Expect.equal + (generatedTemplate.SelectToken("name").ToString()) + "myb2c.onmicrosoft.com" + "`name` should match .onmicrosoft.com" + + Expect.equal + (generatedTemplate + .SelectToken("properties.createTenantProperties.displayName") + .ToString()) + "My B2C tenant" + "Invalid display name" + + Expect.equal + (generatedTemplate.SelectToken("location").ToString()) + "europe" + "`location` should match with the provided `data_residency`" + + Expect.equal + (generatedTemplate + .SelectToken("properties.createTenantProperties.countryCode") + .ToString()) + "FR" + "Invalid country code" + + Expect.equal (generatedTemplate.SelectToken("sku.name").ToString()) "PremiumP1" "Invalid sku" + } + ] diff --git a/src/Tests/Tests.fsproj b/src/Tests/Tests.fsproj index 8ce3d809a..59ee4109a 100644 --- a/src/Tests/Tests.fsproj +++ b/src/Tests/Tests.fsproj @@ -18,6 +18,7 @@ +