diff --git a/docs/resources/wlan.md b/docs/resources/wlan.md index 1e7e9f30b..3e28ea682 100644 --- a/docs/resources/wlan.md +++ b/docs/resources/wlan.md @@ -42,6 +42,7 @@ resource "unifi_wlan" "wifi" { # enable WPA2/WPA3 support wpa3_support = true wpa3_transition = true + pmf_mode = "optional" network_id = unifi_network.vlan.id ap_group_ids = [data.unifi_ap_group.default.id] @@ -73,12 +74,13 @@ resource "unifi_wlan" "wifi" { - **network_id** (String) ID of the network for this SSID - **no2ghz_oui** (Boolean) Connect high performance clients to 5 GHz only Defaults to `true`. - **passphrase** (String, Sensitive) The passphrase for the network, this is only required if `security` is not set to `open`. +- **pmf_mode** (String) Enable Protected Management Frames. This cannot be disabled if using WPA 3. Valid values are `required`, `optional` and `disabled`. Defaults to `disabled`. - **radius_profile_id** (String) ID of the RADIUS profile to use when security `wpaeap`. You can query this via the `unifi_radius_profile` data source. - **schedule** (Block List) Start and stop schedules for the WLAN (see [below for nested schema](#nestedblock--schedule)) - **site** (String) The name of the site to associate the wlan with. - **uapsd** (Boolean) Enable Unscheduled Automatic Power Save Delivery Defaults to `false`. - **wlan_band** (String) Radio band your WiFi network will use. -- **wpa3_support** (Boolean) Enable WPA 3 support (security must be `wpapsk`). +- **wpa3_support** (Boolean) Enable WPA 3 support (security must be `wpapsk` and PMF must be turned on). - **wpa3_transition** (Boolean) Enable WPA 3 and WPA 2 support (security must be `wpapsk` and `wpa3_support` must be true). ### Read-Only diff --git a/examples/resources/unifi_wlan/resource.tf b/examples/resources/unifi_wlan/resource.tf index c978169af..3c826237a 100644 --- a/examples/resources/unifi_wlan/resource.tf +++ b/examples/resources/unifi_wlan/resource.tf @@ -27,6 +27,7 @@ resource "unifi_wlan" "wifi" { # enable WPA2/WPA3 support wpa3_support = true wpa3_transition = true + pmf_mode = "optional" network_id = unifi_network.vlan.id ap_group_ids = [data.unifi_ap_group.default.id] diff --git a/internal/provider/resource_wlan.go b/internal/provider/resource_wlan.go index 1c9b6a069..463ad78f6 100644 --- a/internal/provider/resource_wlan.go +++ b/internal/provider/resource_wlan.go @@ -59,7 +59,7 @@ func resourceWLAN() *schema.Resource { ValidateFunc: validation.StringInSlice([]string{"wpapsk", "wpaeap", "open"}, false), }, "wpa3_support": { - Description: "Enable WPA 3 support (security must be `wpapsk`).", + Description: "Enable WPA 3 support (security must be `wpapsk` and PMF must be turned on).", Type: schema.TypeBool, Optional: true, }, @@ -68,6 +68,13 @@ func resourceWLAN() *schema.Resource { Type: schema.TypeBool, Optional: true, }, + "pmf_mode": { + Description: "Enable Protected Management Frames. This cannot be disabled if using WPA 3. Valid values are `required`, `optional` and `disabled`.", + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"required", "optional", "disabled"}, false), + Default: "disabled", + }, "passphrase": { Description: "The passphrase for the network, this is only required if `security` is not set to `open`.", Type: schema.TypeString, @@ -216,6 +223,7 @@ func resourceWLANGetResourceData(d *schema.ResourceData, meta interface{}) (*uni passphrase = "" } + pmf := d.Get("pmf_mode").(string) wpa3 := d.Get("wpa3_support").(bool) wpa3Transition := d.Get("wpa3_transition").(bool) switch security { @@ -232,6 +240,12 @@ func resourceWLANGetResourceData(d *schema.ResourceData, meta interface{}) (*uni } } + if wpa3Transition && pmf == "disabled" { + return nil, fmt.Errorf("WPA 3 transition mode requires pmf_mode to be turned on.") + } else if wpa3 && !wpa3Transition && pmf != "required" { + return nil, fmt.Errorf("For WPA 3 you must set pmf_mode to required.") + } + macFilterEnabled := d.Get("mac_filter_enabled").(bool) macFilterList, err := setToStringSlice(d.Get("mac_filter_list").(*schema.Set)) if err != nil { @@ -274,6 +288,7 @@ func resourceWLANGetResourceData(d *schema.ResourceData, meta interface{}) (*uni Schedule: schedule, ScheduleEnabled: len(schedule) > 0, WLANBand: wlanBand, + PMFMode: pmf, // TODO: add to schema WPAEnc: "ccmp", @@ -371,7 +386,7 @@ func resourceWLANSetResourceData(resp *unifi.WLAN, d *schema.ResourceData, meta d.Set("uapsd", resp.UapsdEnabled) d.Set("ap_group_ids", apGroupIDs) d.Set("network_id", resp.NetworkID) - + d.Set("pmf_mode", resp.PMFMode) if resp.MinrateNgEnabled { d.Set("minimum_data_rate_2g_kbps", resp.MinrateNgDataRateKbps) } else { diff --git a/internal/provider/resource_wlan_test.go b/internal/provider/resource_wlan_test.go index c0297a051..b2c889311 100644 --- a/internal/provider/resource_wlan_test.go +++ b/internal/provider/resource_wlan_test.go @@ -53,7 +53,7 @@ func TestAccWLAN_wpapsk(t *testing.T) { }, Steps: []resource.TestStep{ { - Config: testAccWLANConfig_wpapsk(vlanID), + Config: testAccWLANConfig_wpapsk(vlanID, "disabled"), Check: resource.ComposeTestCheckFunc( // testCheckNetworkExists(t, "name"), ), @@ -103,7 +103,7 @@ func TestAccWLAN_open(t *testing.T) { }) } -func TestAccWLAN_change_security(t *testing.T) { +func TestAccWLAN_change_security_and_pmf(t *testing.T) { vlanID := getTestVLAN(t) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { @@ -119,7 +119,7 @@ func TestAccWLAN_change_security(t *testing.T) { }, Steps: []resource.TestStep{ { - Config: testAccWLANConfig_wpapsk(vlanID), + Config: testAccWLANConfig_wpapsk(vlanID, "disabled"), Check: resource.ComposeTestCheckFunc( // testCheckNetworkExists(t, "name"), ), @@ -133,11 +133,26 @@ func TestAccWLAN_change_security(t *testing.T) { }, importStep("unifi_wlan.test"), { - Config: testAccWLANConfig_wpapsk(vlanID), + Config: testAccWLANConfig_wpapsk(vlanID, "optional"), Check: resource.ComposeTestCheckFunc( // testCheckNetworkExists(t, "name"), ), }, + importStep("unifi_wlan.test"), + { + Config: testAccWLANConfig_wpapsk(vlanID, "required"), + Check: resource.ComposeTestCheckFunc( + // testCheckNetworkExists(t, "name"), + ), + }, + importStep("unifi_wlan.test"), + { + Config: testAccWLANConfig_wpapsk(vlanID, "disabled"), + Check: resource.ComposeTestCheckFunc( + // testCheckNetworkExists(t, "name"), + ), + }, + importStep("unifi_wlan.test"), }, }) } @@ -294,21 +309,21 @@ func TestAccWLAN_wpa3(t *testing.T) { }, Steps: []resource.TestStep{ { - Config: testAccWLANConfig_wpa3(vlanID, false), + Config: testAccWLANConfig_wpa3(vlanID, false, "required"), Check: resource.ComposeTestCheckFunc( // testCheckNetworkExists(t, "name"), ), }, importStep("unifi_wlan.test"), { - Config: testAccWLANConfig_wpa3(vlanID, true), + Config: testAccWLANConfig_wpa3(vlanID, true, "optional"), Check: resource.ComposeTestCheckFunc( // testCheckNetworkExists(t, "name"), ), }, importStep("unifi_wlan.test"), { - Config: testAccWLANConfig_wpa3(vlanID, false), + Config: testAccWLANConfig_wpa3(vlanID, false, "required"), Check: resource.ComposeTestCheckFunc( // testCheckNetworkExists(t, "name"), ), @@ -365,7 +380,7 @@ func TestAccWLAN_minimum_data_rate(t *testing.T) { }) } -func testAccWLANConfig_wpapsk(vlanID int) string { +func testAccWLANConfig_wpapsk(vlanID int, pmf string) string { return fmt.Sprintf(` data "unifi_ap_group" "default" { } @@ -390,8 +405,10 @@ resource "unifi_wlan" "test" { security = "wpapsk" multicast_enhance = true + + pmf_mode = %[2]q } -`, vlanID) +`, vlanID, pmf) } func testAccWLANConfig_wpaeap(vlanID int) string { @@ -608,7 +625,7 @@ resource "unifi_wlan" "test" { `, vlanID) } -func testAccWLANConfig_wpa3(vlanID int, wpa3Transition bool) string { +func testAccWLANConfig_wpa3(vlanID int, wpa3Transition bool, pmf string) string { return fmt.Sprintf(` data "unifi_ap_group" "default" { } @@ -634,8 +651,9 @@ resource "unifi_wlan" "test" { wpa3_support = true wpa3_transition = %[2]t + pmf_mode = %[3]q } -`, vlanID, wpa3Transition) +`, vlanID, wpa3Transition, pmf) } func testAccWLANConfig_minimum_data_rate(vlanID int, min2g int, min5g int) string {