diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 39d3f1e3a..29424df71 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,6 +3,7 @@ Release Notes ## 1.7.25 * Network Security Groups: Fix bug where a SecurityRule without a source throws a meaningful exception * Network Security Groups: Add rule to existing security group +* SQL Azure: Adds support for AD admin ## 1.7.24 * Network Interface: Adds support for network interface creation. diff --git a/docs/content/api-overview/resources/sql.md b/docs/content/api-overview/resources/sql.md index bad3c487b..87e0eeb40 100644 --- a/docs/content/api-overview/resources/sql.md +++ b/docs/content/api-overview/resources/sql.md @@ -11,20 +11,29 @@ The SQL Azure module contains two builders - `sqlServer`, used to create SQL Azu * SQL Azure server (`Microsoft.Sql/servers`) #### SQL Server Builder Keywords -| Keyword | Purpose | -|-|-| -| name | Sets the name of the SQL server. | -| add_firewall_rule | Adds a custom firewall rule given a name, start and end IP address range. | -| add_firewall_rules | As add_firewall_rule but a list of rules | -| enable_azure_firewall | Adds a firewall rule that enables access to other Azure services. | -| admin_username | Sets the admin username of the server. | -| elastic_pool_name | Sets the name of the elastic pool, if required. If not set, Farmer will generate a name for you. | -| elastic_pool_sku | Sets the sku of the elastic pool, if required. If not set, Farmer will default to Basic 50. | -| elastic_pool_database_min_max | Sets the optional minimum and maximum DTUs for the elastic pool for each database. | -| elastic_pool_capacity | Sets the optional disk size in MB for the elastic pool for each database. | -| min_tls_version | Sets the minium TLS version for the SQL server | +| Keyword | Purpose | +|-|---------------------------------------------------------------------------------------------------------------------------------| +| name | Sets the name of the SQL server. | +| active_directory_admin | Sets Active Directory admin of the server | +| add_firewall_rule | Adds a custom firewall rule given a name, start and end IP address range. | +| add_firewall_rules | As add_firewall_rule but a list of rules | +| enable_azure_firewall | Adds a firewall rule that enables access to other Azure services. | +| admin_username | Sets the admin username of the server. | +| elastic_pool_name | Sets the name of the elastic pool, if required. If not set, Farmer will generate a name for you. | +| elastic_pool_sku | Sets the sku of the elastic pool, if required. If not set, Farmer will default to Basic 50. | +| elastic_pool_database_min_max | Sets the optional minimum and maximum DTUs for the elastic pool for each database. | +| elastic_pool_capacity | Sets the optional disk size in MB for the elastic pool for each database. | +| min_tls_version | Sets the minium TLS version for the SQL server | | geo_replicate | Geo-replicate all the databases in this server to another location, having NameSuffix after original server and database names. | +#### ActiveDirectoryAdminSettings Members +| Member | Purpose | +|-|----------------------------------------------------------------------------| +| Login | Display name of AD admin | +| Sid | AD object id of AD admin (user or group) | +| PrincipalType | ActiveDirectoryPrincipalType User or Group | +| AdOnlyAuth | Disables SQL authentication. False value required admin_username to be set | + #### SQL Server Configuration Members | Member | Purpose | |-|-| @@ -43,6 +52,8 @@ The SQL Azure module contains two builders - `sqlServer`, used to create SQL Azu | use_encryption | Enables transparent data encryption of the database. | #### Example + +##### AD auth not set ```fsharp open Farmer open Farmer.Builders @@ -84,3 +95,57 @@ template template |> Deploy.execute "my-resource-group" [ "password-for-my_server", "*****" ] ``` + +##### AD auth set +```fsharp +open Farmer +open Farmer.Builders +open Sql +open Farmer.Arm.Sql + +let activeDirectoryAdmin: ActiveDirectoryAdminSettings = + { + Login = "adadmin" + Sid = "F9D49C34-01BA-4897-B7E2-3694BF3DE2CF" + PrincipalType = ActiveDirectoryPrincipalType.User + AdOnlyAuth = false // when false, admin_username is required + // when true admin_username is ignored + } + +let myDatabases = sqlServer { + name "my_server" + active_directory_admin (Some(activeDirectoryAdmin)) + admin_username "admin_username" + enable_azure_firewall + + elastic_pool_name "mypool" + elastic_pool_sku PoolSku.Basic100 + + add_databases [ + sqlDb { name "poolDb1" } + sqlDb { name "poolDb2" } + sqlDb { name "dtuDb"; sku Basic } + sqlDb { name "memoryDb"; sku M_8 } + sqlDb { name "cpuDb"; sku Fsv2_8 } + sqlDb { name "businessCriticalDb"; sku (BusinessCritical Gen5_2) } + sqlDb { name "hyperscaleDb"; sku (Hyperscale Gen5_2) } + sqlDb { + name "generalPurposeDb" + sku (GeneralPurpose Gen5_8) + db_size (1024 * 128) + hybrid_benefit + } + ] +} + +let template = arm { + location Location.NorthEurope + add_resource myDatabases +} + +template +|> Writer.quickWrite "sql-example" + +template +|> Deploy.execute "my-resource-group" [ "password-for-my_server", "*****" ] +``` \ No newline at end of file diff --git a/src/Farmer/Arm/Sql.fs b/src/Farmer/Arm/Sql.fs index c061f2d3f..57cb3845a 100644 --- a/src/Farmer/Arm/Sql.fs +++ b/src/Farmer/Arm/Sql.fs @@ -5,7 +5,7 @@ open Farmer open Farmer.Sql open System.Net -let servers = ResourceType("Microsoft.Sql/servers", "2019-06-01-preview") +let servers = ResourceType("Microsoft.Sql/servers", "2022-05-01-preview") let elasticPools = ResourceType("Microsoft.Sql/servers/elasticPools", "2017-10-01-preview") @@ -23,18 +23,111 @@ type DbKind = | Standalone of DbPurchaseModel | Pool of ResourceName +type ActiveDirectoryPrincipalType = + | User + | Group + +type ActiveDirectoryAdminSettings = + { + /// Ideally same as AD name + Login: string + /// Active Directory object id of user or group + Sid: string + PrincipalType: ActiveDirectoryPrincipalType + AdOnlyAuth: bool + } + +let (|MixedModeAuth|AdOnlyAuth|SqlOnlyAuth|) activeDirAdmin = + match activeDirAdmin with + | Some x when x.AdOnlyAuth -> AdOnlyAuth(x) + | Some x when not x.AdOnlyAuth -> MixedModeAuth(x) + | _ -> SqlOnlyAuth + +type SqlServerADAdminJsonProperties = + { + administratorType: string + principalType: string + login: string + sid: string + azureADOnlyAuthentication: bool + } + +type SqlServerJsonProperties = + { + version: string + minimalTlsVersion: string + administratorLogin: string + administratorLoginPassword: string + administrators: SqlServerADAdminJsonProperties + } + type Server = { ServerName: SqlAccountName Location: Location Credentials: {| Username: string Password: SecureParameter |} + ActiveDirectoryAdmin: ActiveDirectoryAdminSettings option MinTlsVersion: TlsVersion option Tags: Map } + member private this.BuildSqlSeverPropertiesBase() : SqlServerJsonProperties = + { + version = "12.0" + minimalTlsVersion = + match this.MinTlsVersion with + | Some Tls10 -> "1.0" + | Some Tls11 -> "1.1" + | Some Tls12 -> "1.2" + | None -> null + administratorLogin = null + administratorLoginPassword = null + administrators = Unchecked.defaultof + } + + member private this.BuildSqlServerADOnlyAdmin(x: ActiveDirectoryAdminSettings) : SqlServerADAdminJsonProperties = + { + administratorType = "ActiveDirectory" + principalType = + match x.PrincipalType with + | Group -> "Group" + | User -> "User" + login = x.Login + sid = x.Sid + azureADOnlyAuthentication = true + } + + member private this.BuildSqlServerPropertiesWithMixedModeAdministrator + (x: ActiveDirectoryAdminSettings) + : SqlServerJsonProperties = + { this.BuildSqlSeverPropertiesBase() with + administratorLogin = this.Credentials.Username + administratorLoginPassword = this.Credentials.Password.ArmExpression.Eval() + administrators = + { this.BuildSqlServerADOnlyAdmin(x) with + azureADOnlyAuthentication = false + } + } + + member private this.BuildSqlServerPropertiesWithADOnlyAdministrator + (x: ActiveDirectoryAdminSettings) + : SqlServerJsonProperties = + { this.BuildSqlSeverPropertiesBase() with + administrators = this.BuildSqlServerADOnlyAdmin(x) + } + + member private this.BuildSqlServerPropertiesWithSqlOnlyAdministrator() : SqlServerJsonProperties = + { this.BuildSqlSeverPropertiesBase() with + administratorLogin = this.Credentials.Username + administratorLoginPassword = this.Credentials.Password.ArmExpression.Eval() + } + interface IParameters with - member this.SecureParameters = [ this.Credentials.Password ] + member this.SecureParameters = + match this.ActiveDirectoryAdmin with + | Some (x) when x.AdOnlyAuth -> [] + | _ -> [ this.Credentials.Password ] interface IArmResource with member this.ResourceId = servers.resourceId this.ServerName.ResourceName @@ -46,17 +139,10 @@ type Server = tags = (this.Tags |> Map.add "displayName" this.ServerName.ResourceName.Value) ) with properties = - {| - administratorLogin = this.Credentials.Username - administratorLoginPassword = this.Credentials.Password.ArmExpression.Eval() - version = "12.0" - minimalTlsVersion = - match this.MinTlsVersion with - | Some Tls10 -> "1.0" - | Some Tls11 -> "1.1" - | Some Tls12 -> "1.2" - | None -> null - |} + match this.ActiveDirectoryAdmin with + | MixedModeAuth x -> this.BuildSqlServerPropertiesWithMixedModeAdministrator(x) + | AdOnlyAuth x -> this.BuildSqlServerPropertiesWithADOnlyAdministrator(x) + | SqlOnlyAuth -> this.BuildSqlServerPropertiesWithSqlOnlyAdministrator() |} module Servers = diff --git a/src/Farmer/Builders/Builders.ContainerService.fs b/src/Farmer/Builders/Builders.ContainerService.fs index 2575198bb..7265568f5 100644 --- a/src/Farmer/Builders/Builders.ContainerService.fs +++ b/src/Farmer/Builders/Builders.ContainerService.fs @@ -237,7 +237,8 @@ type AgentPoolBuilder() = /// Sets the agent pool to user mode. [] - member _.UserMode(state: AgentPoolConfig) = { state with Mode = User } + member _.UserMode(state: AgentPoolConfig) = + { state with Mode = AgentPoolMode.User } /// Sets the disk size for the VM's in the agent pool. [] diff --git a/src/Farmer/Builders/Builders.Sql.fs b/src/Farmer/Builders/Builders.Sql.fs index ef0de5830..40743725c 100644 --- a/src/Farmer/Builders/Builders.Sql.fs +++ b/src/Farmer/Builders/Builders.Sql.fs @@ -22,6 +22,7 @@ type SqlAzureConfig = Name: SqlAccountName AdministratorCredentials: {| UserName: string Password: SecureParameter |} + ActiveDirectoryAdmin: ActiveDirectoryAdminSettings option MinTlsVersion: TlsVersion option FirewallRules: {| Name: ResourceName Start: IPAddress @@ -74,10 +75,14 @@ type SqlAzureConfig = ServerName = this.Name Location = location Credentials = - {| - Username = this.AdministratorCredentials.UserName - Password = this.AdministratorCredentials.Password - |} + match this.ActiveDirectoryAdmin with + | AdOnlyAuth _ -> Unchecked.defaultof<_> + | _ -> + {| + Username = this.AdministratorCredentials.UserName + Password = this.AdministratorCredentials.Password + |} + ActiveDirectoryAdmin = this.ActiveDirectoryAdmin MinTlsVersion = this.MinTlsVersion Tags = this.Tags } @@ -144,6 +149,7 @@ type SqlAzureConfig = Username = this.AdministratorCredentials.UserName Password = this.AdministratorCredentials.Password |} + ActiveDirectoryAdmin = this.ActiveDirectoryAdmin MinTlsVersion = this.MinTlsVersion Tags = this.Tags } @@ -277,6 +283,7 @@ type SqlServerBuilder() = UserName = "" Password = SecureParameter "" |} + ActiveDirectoryAdmin = None ElasticPoolSettings = {| Name = None @@ -295,16 +302,25 @@ type SqlServerBuilder() = if state.Name.ResourceName = ResourceName.Empty then raiseFarmer "No SQL Server account name has been set." - { state with - AdministratorCredentials = - if System.String.IsNullOrWhiteSpace state.AdministratorCredentials.UserName then - raiseFarmer - $"You must specify the admin_username for SQL Server instance {state.Name.ResourceName.Value}" + let getStateWithAdminCredentials () = + if System.String.IsNullOrWhiteSpace state.AdministratorCredentials.UserName then + raiseFarmer + $"You must specify the admin_username for SQL Server instance {state.Name.ResourceName.Value}" - {| state.AdministratorCredentials with - Password = SecureParameter state.PasswordParameter - |} - } + { state with + AdministratorCredentials = + {| state.AdministratorCredentials with + Password = SecureParameter state.PasswordParameter + |} + } + + match state.ActiveDirectoryAdmin with + | AdOnlyAuth _ -> + { state with + AdministratorCredentials = Unchecked.defaultof<_> + } + | MixedModeAuth _ -> getStateWithAdminCredentials () + | SqlOnlyAuth -> getStateWithAdminCredentials () /// Sets the name of the SQL server. [] @@ -420,6 +436,13 @@ type SqlServerBuilder() = GeoReplicaServer = Some replicaSettings } + /// Sets the active directory admin and optionally turns on AD only auth. + [] + member _.SetActiveDirectoryAdmin(state: SqlAzureConfig, activeDirectoryAdminSettings) = + { state with + ActiveDirectoryAdmin = activeDirectoryAdminSettings + } + interface ITaggable with member _.Add state tags = { state with diff --git a/src/Tests/AllTests.fs b/src/Tests/AllTests.fs index f5eb71ae7..0ac8df4d4 100644 --- a/src/Tests/AllTests.fs +++ b/src/Tests/AllTests.fs @@ -96,8 +96,4 @@ let allTests = let main _ = printfn "Running tests!" - runTests - { defaultConfig with - verbosity = Logging.Info - } - allTests + Tests.runTestsWithCLIArgs [] [| (*"--debug"*) |] allTests diff --git a/src/Tests/Sql.fs b/src/Tests/Sql.fs index df261e75e..23b5e3a91 100644 --- a/src/Tests/Sql.fs +++ b/src/Tests/Sql.fs @@ -2,6 +2,7 @@ module Sql open Expecto open Farmer +open Farmer.Arm open Farmer.Sql open Farmer.Builders open Microsoft.Azure.Management.Sql @@ -210,6 +211,7 @@ let tests = check "-zz" "cannot start with a dash (-). The invalid value is '-zz'" "Start with dash" check "zz-" "cannot end with a dash (-). The invalid value is 'zz-'" "End with dash" } + test "Sets Min TLS version correctly" { let sql = sqlServer { @@ -324,10 +326,80 @@ let tests = "Incorrect autoPauseDelay" } - test "Must set a SQL Server account name" { Expect.throws (fun () -> sqlServer { admin_username "test" } |> ignore) "Must set a name on a sql server account" } + + + testTheory + "AD Auth" + [ + (true, ActiveDirectoryPrincipalType.User, null) + (false, ActiveDirectoryPrincipalType.User, "sqladmin") + (true, ActiveDirectoryPrincipalType.Group, null) + (false, ActiveDirectoryPrincipalType.Group, "sqladmin") + ] + <| fun (adOnlyAuth, principalType, adminUserName) -> + let sql = + let activeDirectoryUserAdmin: ActiveDirectoryAdminSettings = + { + Login = "adadmin" + Sid = "F9D49C34-01BA-4897-B7E2-3694BF3DE2CF" + PrincipalType = principalType + AdOnlyAuth = adOnlyAuth + } + + sqlServer { + name "adtestserver" + active_directory_admin (Some(activeDirectoryUserAdmin)) + admin_username adminUserName + } + + let template = + arm { + location Location.AustraliaEast + add_resources [ sql ] + } + + let jsn = template.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='adtestserver')].properties.administrators.administratorType") + .ToString()) + "ActiveDirectory" + "Incorrect administrator type" + + Expect.equal + (jobj + .SelectToken( + "resources[?(@.name=='adtestserver')].properties.administrators.azureADOnlyAuthentication" + ) + .ToString()) + (adOnlyAuth.ToString()) + $"AD only auth should be {adOnlyAuth.ToString()}" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='adtestserver')].properties.administrators.login") + .ToString()) + "adadmin" + "Incorrect AD login name" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='adtestserver')].properties.administrators.principalType") + .ToString()) + $"{principalType.ToString()}" + "Incorrect principal type" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='adtestserver')].properties.administrators.sid") + .ToString()) + "F9D49C34-01BA-4897-B7E2-3694BF3DE2CF" + "Incorrect SID" ] diff --git a/src/Tests/Tests.fsproj b/src/Tests/Tests.fsproj index 8617909a9..c13847f94 100644 --- a/src/Tests/Tests.fsproj +++ b/src/Tests/Tests.fsproj @@ -73,7 +73,7 @@ - + @@ -100,8 +100,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + diff --git a/src/Tests/test-data/lots-of-resources.json b/src/Tests/test-data/lots-of-resources.json index a6619d5ac..d4ede2438 100644 --- a/src/Tests/test-data/lots-of-resources.json +++ b/src/Tests/test-data/lots-of-resources.json @@ -12,13 +12,13 @@ }, "resources": [ { - "apiVersion": "2019-06-01-preview", + "apiVersion": "2022-05-01-preview", "location": "northeurope", "name": "farmersql1979", "properties": { + "version": "12.0", "administratorLogin": "farmersqladmin", - "administratorLoginPassword": "[parameters('password-for-farmersql1979')]", - "version": "12.0" + "administratorLoginPassword": "[parameters('password-for-farmersql1979')]" }, "tags": { "displayName": "farmersql1979"