feat: create users in the build-in radius server (#286)

* feat: account resource with data source

* Adjust docs and validation

* add import test steps

* adjust radius capitalization in docs

Co-authored-by: Paul Tyng <paul@paultyng.net>
This commit is contained in:
Oskar
2022-10-23 16:12:10 +02:00
committed by GitHub
parent 6f4f1058e4
commit 0cf907be5f
12 changed files with 520 additions and 13 deletions

View File

@@ -0,0 +1,34 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "unifi_account Data Source - terraform-provider-unifi"
subcategory: ""
description: |-
unifi_account data source can be used to retrieve RADIUS user accounts
---
# unifi_account (Data Source)
`unifi_account` data source can be used to retrieve RADIUS user accounts
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `name` (String) The name of the account to look up
### Optional
- `site` (String) The name of the site the account is associated with.
### Read-Only
- `id` (String) The ID of this account.
- `network_id` (String) ID of the network for this account
- `password` (String, Sensitive) The password of the account.
- `tunnel_medium_type` (Number) See RFC2868 section 3.2
- `tunnel_type` (Number) See RFC2868 section 3.1

View File

@@ -18,7 +18,7 @@ description: |-
### Optional
- `name` (String) The name of the RADIUS profile to look up. Defaults to `Default`.
- `site` (String) The name of the site the radius profile is associated with.
- `site` (String) The name of the site the RADIUS profile is associated with.
### Read-Only

45
docs/resources/account.md Normal file
View File

@@ -0,0 +1,45 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "unifi_account Resource - terraform-provider-unifi"
subcategory: ""
description: |-
unifi_account manages a RADIUS user account
To authenticate devices based on MAC address, use the MAC address as the username and password under client creation.
Convert lowercase letters to uppercase, and also remove colons or periods from the MAC address.
ATTENTION: If the user profile does not include a VLAN, the client will fall back to the untagged VLAN.
NOTE: MAC-based authentication accounts can only be used for wireless and wired clients. L2TP remote access does not apply.
---
# unifi_account (Resource)
`unifi_account` manages a RADIUS user account
To authenticate devices based on MAC address, use the MAC address as the username and password under client creation.
Convert lowercase letters to uppercase, and also remove colons or periods from the MAC address.
ATTENTION: If the user profile does not include a VLAN, the client will fall back to the untagged VLAN.
NOTE: MAC-based authentication accounts can only be used for wireless and wired clients. L2TP remote access does not apply.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `name` (String) The name of the account.
- `password` (String, Sensitive) The password of the account.
### Optional
- `network_id` (String) ID of the network for this account
- `site` (String) The name of the site to associate the account with.
- `tunnel_medium_type` (Number) See [RFC 2868](https://www.rfc-editor.org/rfc/rfc2868) section 3.2 Defaults to `6`.
- `tunnel_type` (Number) See [RFC 2868](https://www.rfc-editor.org/rfc/rfc2868) section 3.1 Defaults to `13`.
### Read-Only
- `id` (String) The ID of the account.

View File

@@ -3,12 +3,12 @@
page_title: "unifi_radius_profile Resource - terraform-provider-unifi"
subcategory: ""
description: |-
unifi_radius_profile manages radius profiles.
unifi_radius_profile manages RADIUS profiles.
---
# unifi_radius_profile (Resource)
`unifi_radius_profile` manages radius profiles.
`unifi_radius_profile` manages RADIUS profiles.
@@ -21,14 +21,14 @@ description: |-
### Optional
- `accounting_enabled` (Boolean) Specifies whether to use radius accounting. Defaults to `false`.
- `accounting_enabled` (Boolean) Specifies whether to use RADIUS accounting. Defaults to `false`.
- `acct_server` (Block List) RADIUS accounting servers. (see [below for nested schema](#nestedblock--acct_server))
- `auth_server` (Block List) RADIUS authentication servers. (see [below for nested schema](#nestedblock--auth_server))
- `interim_update_enabled` (Boolean) Specifies whether to use interim_update. Defaults to `false`.
- `interim_update_interval` (Number) Specifies interim_update interval. Defaults to `3600`.
- `site` (String) The name of the site to associate the settings with.
- `use_usg_acct_server` (Boolean) Specifies whether to use usg as a radius accounting server. Defaults to `false`.
- `use_usg_auth_server` (Boolean) Specifies whether to use usg as a radius authentication server. Defaults to `false`.
- `use_usg_acct_server` (Boolean) Specifies whether to use usg as a RADIUS accounting server. Defaults to `false`.
- `use_usg_auth_server` (Boolean) Specifies whether to use usg as a RADIUS authentication server. Defaults to `false`.
- `vlan_enabled` (Boolean) Specifies whether to use vlan on wired connections. Defaults to `false`.
- `vlan_wlan_mode` (String) Specifies whether to use vlan on wireless connections. Must be one of `disabled`, `optional`, or `required`. Defaults to ``.

View File

@@ -0,0 +1,86 @@
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
func dataAccount() *schema.Resource {
return &schema.Resource{
Description: "`unifi_account` data source can be used to retrieve RADIUS user accounts",
ReadContext: dataAccountRead,
Schema: map[string]*schema.Schema{
"id": {
Description: "The ID of this account.",
Type: schema.TypeString,
Computed: true,
},
"site": {
Description: "The name of the site the account is associated with.",
Type: schema.TypeString,
Computed: true,
Optional: true,
},
"name": {
Description: "The name of the account to look up",
Type: schema.TypeString,
Required: true,
},
"password": {
Description: "The password of the account.",
Type: schema.TypeString,
Computed: true,
Sensitive: true,
},
"tunnel_type": {
Description: "See RFC2868 section 3.1", // @TODO: better documentation https://help.ui.com/hc/en-us/articles/360015268353-UniFi-USG-UDM-Configuring-RADIUS-Server#6
Type: schema.TypeInt,
Computed: true,
},
"tunnel_medium_type": {
Description: "See RFC2868 section 3.2", // @TODO: better documentation https://help.ui.com/hc/en-us/articles/360015268353-UniFi-USG-UDM-Configuring-RADIUS-Server#6
Type: schema.TypeInt,
Computed: true,
},
"network_id": {
Description: "ID of the network for this account",
Type: schema.TypeString,
Computed: true,
},
},
}
}
func dataAccountRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*client)
name := d.Get("name").(string)
site := d.Get("site").(string)
if site == "" {
site = c.site
}
accounts, err := c.c.ListAccounts(ctx, site)
if err != nil {
return diag.FromErr(err)
}
for _, account := range accounts {
if account.Name == name {
d.SetId(account.ID)
d.Set("name", account.Name)
d.Set("password", account.XPassword)
d.Set("tunnel_type", account.TunnelType)
d.Set("tunnel_medium_type", account.TunnelMediumType)
d.Set("network_id", account.NetworkID)
d.Set("site", site)
return nil
}
}
return diag.Errorf("Account not found with name %s", name)
}

View File

@@ -0,0 +1,71 @@
package provider
import (
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"testing"
)
func TestAccDataAccount_default(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
preCheck(t)
},
ProviderFactories: providerFactories,
// TODO: CheckDestroy: ,
Steps: []resource.TestStep{
{
Config: testAccDataAccountConfig("tfusertest", "secure_1234"),
Check: resource.ComposeTestCheckFunc(),
},
},
})
}
func TestAccDataAccount_mac(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
preCheck(t)
},
ProviderFactories: providerFactories,
// TODO: CheckDestroy: ,
Steps: []resource.TestStep{
{
Config: testAccDataMacAccountConfig("00B0D06FC226"),
Check: resource.ComposeTestCheckFunc(),
},
},
})
}
func testAccDataAccountConfig(name, password string) string {
return fmt.Sprintf(`
resource "unifi_account" "test" {
name = "%s"
password = "%s"
}
data "unifi_account" "test" {
name = "%s"
depends_on = [
unifi_account.test
]
}
`, name, password, name)
}
func testAccDataMacAccountConfig(mac string) string {
return fmt.Sprintf(`
resource "unifi_account" "test" {
name = "%s"
password = "%s"
}
data "unifi_account" "test" {
name = "%s"
depends_on = [
unifi_account.test
]
}
`, mac, mac, mac)
}

View File

@@ -20,7 +20,7 @@ func dataRADIUSProfile() *schema.Resource {
Computed: true,
},
"site": {
Description: "The name of the site the radius profile is associated with.",
Description: "The name of the site the RADIUS profile is associated with.",
Type: schema.TypeString,
Computed: true,
Optional: true,

View File

@@ -369,6 +369,36 @@ func (c *lazyClient) UpdateRADIUSProfile(ctx context.Context, site string, d *un
}
return c.inner.UpdateRADIUSProfile(ctx, site, d)
}
func (c *lazyClient) ListAccounts(ctx context.Context, site string) ([]unifi.Account, error) {
if err := c.init(ctx); err != nil {
return nil, err
}
return c.inner.ListAccount(ctx, site)
}
func (c *lazyClient) GetAccount(ctx context.Context, site, id string) (*unifi.Account, error) {
if err := c.init(ctx); err != nil {
return nil, err
}
return c.inner.GetAccount(ctx, site, id)
}
func (c *lazyClient) DeleteAccount(ctx context.Context, site, id string) error {
if err := c.init(ctx); err != nil {
return err
}
return c.inner.DeleteAccount(ctx, site, id)
}
func (c *lazyClient) CreateAccount(ctx context.Context, site string, d *unifi.Account) (*unifi.Account, error) {
if err := c.init(ctx); err != nil {
return nil, err
}
return c.inner.CreateAccount(ctx, site, d)
}
func (c *lazyClient) UpdateAccount(ctx context.Context, site string, d *unifi.Account) (*unifi.Account, error) {
if err := c.init(ctx); err != nil {
return nil, err
}
return c.inner.UpdateAccount(ctx, site, d)
}
func (c *lazyClient) GetSite(ctx context.Context, id string) (*unifi.Site, error) {
if err := c.init(ctx); err != nil {
return nil, err

View File

@@ -75,6 +75,7 @@ func New(version string) func() *schema.Provider {
"unifi_radius_profile": dataRADIUSProfile(),
"unifi_user_group": dataUserGroup(),
"unifi_user": dataUser(),
"unifi_account": dataAccount(),
},
ResourcesMap: map[string]*schema.Resource{
// TODO: "unifi_ap_group"
@@ -91,6 +92,7 @@ func New(version string) func() *schema.Provider {
"unifi_user_group": resourceUserGroup(),
"unifi_user": resourceUser(),
"unifi_wlan": resourceWLAN(),
"unifi_account": resourceAccount(),
"unifi_setting_mgmt": resourceSettingMgmt(),
"unifi_setting_radius": resourceSettingRadius(),
@@ -187,6 +189,12 @@ type unifiClient interface {
CreateRADIUSProfile(ctx context.Context, site string, d *unifi.RADIUSProfile) (*unifi.RADIUSProfile, error)
UpdateRADIUSProfile(ctx context.Context, site string, d *unifi.RADIUSProfile) (*unifi.RADIUSProfile, error)
ListAccounts(ctx context.Context, site string) ([]unifi.Account, error)
GetAccount(ctx context.Context, site, id string) (*unifi.Account, error)
DeleteAccount(ctx context.Context, site, id string) error
CreateAccount(ctx context.Context, site string, d *unifi.Account) (*unifi.Account, error)
UpdateAccount(ctx context.Context, site string, d *unifi.Account) (*unifi.Account, error)
GetSite(ctx context.Context, id string) (*unifi.Site, error)
ListSites(ctx context.Context) ([]unifi.Site, error)
CreateSite(ctx context.Context, Description string) ([]unifi.Site, error)

View File

@@ -0,0 +1,179 @@
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/paultyng/go-unifi/unifi"
)
func resourceAccount() *schema.Resource {
return &schema.Resource{
Description: "`unifi_account` manages a RADIUS user account\n\n" +
"To authenticate devices based on MAC address, use the MAC address as the username and password under client creation. \n" +
"Convert lowercase letters to uppercase, and also remove colons or periods from the MAC address. \n\n" +
"ATTENTION: If the user profile does not include a VLAN, the client will fall back to the untagged VLAN. \n\n" +
"NOTE: MAC-based authentication accounts can only be used for wireless and wired clients. L2TP remote access does not apply.",
CreateContext: resourceAccountCreate,
ReadContext: resourceAccountRead,
UpdateContext: resourceAccountUpdate,
DeleteContext: resourceAccountDelete,
Importer: &schema.ResourceImporter{
StateContext: importSiteAndID,
},
Schema: map[string]*schema.Schema{
"id": {
Description: "The ID of the account.",
Type: schema.TypeString,
Computed: true,
},
"site": {
Description: "The name of the site to associate the account with.",
Type: schema.TypeString,
Computed: true,
Optional: true,
ForceNew: true,
},
"name": {
Description: "The name of the account.",
Type: schema.TypeString,
Required: true,
},
"password": {
Description: "The password of the account.",
Type: schema.TypeString,
Required: true,
Sensitive: true,
},
"tunnel_type": {
Description: "See [RFC 2868](https://www.rfc-editor.org/rfc/rfc2868) section 3.1", // @TODO: better documentation https://help.ui.com/hc/en-us/articles/360015268353-UniFi-USG-UDM-Configuring-RADIUS-Server#6
Type: schema.TypeInt,
Optional: true,
Default: 13,
ValidateFunc: validation.IntBetween(1, 13),
},
"tunnel_medium_type": {
Description: "See [RFC 2868](https://www.rfc-editor.org/rfc/rfc2868) section 3.2", // @TODO: better documentation https://help.ui.com/hc/en-us/articles/360015268353-UniFi-USG-UDM-Configuring-RADIUS-Server#6
Type: schema.TypeInt,
Optional: true,
Default: 6,
ValidateFunc: validation.IntBetween(1, 15),
},
"network_id": {
Description: "ID of the network for this account",
Type: schema.TypeString,
Optional: true,
},
},
}
}
func resourceAccountCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*client)
req, err := resourceAccountGetResourceData(d)
if err != nil {
return diag.FromErr(err)
}
site := d.Get("site").(string)
if site == "" {
site = c.site
}
resp, err := c.c.CreateAccount(ctx, site, req)
if err != nil {
return diag.FromErr(err)
}
d.SetId(resp.ID)
return resourceAccountSetResourceData(resp, d, site)
}
func resourceAccountUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*client)
site := d.Get("site").(string)
if site == "" {
site = c.site
}
req, err := resourceAccountGetResourceData(d)
if err != nil {
return diag.FromErr(err)
}
req.ID = d.Id()
req.SiteID = site
resp, err := c.c.UpdateAccount(ctx, site, req)
if err != nil {
return diag.FromErr(err)
}
return resourceAccountSetResourceData(resp, d, site)
}
func resourceAccountDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*client)
//name := d.Get("name").(string)
site := d.Get("site").(string)
if site == "" {
site = c.site
}
id := d.Id()
err := c.c.DeleteAccount(ctx, site, id)
if _, ok := err.(*unifi.NotFoundError); ok {
return nil
}
return diag.FromErr(err)
}
func resourceAccountRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*client)
id := d.Id()
site := d.Get("site").(string)
if site == "" {
site = c.site
}
resp, err := c.c.GetAccount(ctx, site, id)
if _, ok := err.(*unifi.NotFoundError); ok {
d.SetId("")
return nil
}
if err != nil {
return diag.FromErr(err)
}
return resourceAccountSetResourceData(resp, d, site)
}
func resourceAccountSetResourceData(resp *unifi.Account, d *schema.ResourceData, site string) diag.Diagnostics {
d.Set("site", site)
d.Set("name", resp.Name)
d.Set("password", resp.XPassword)
d.Set("tunnel_type", resp.TunnelType)
d.Set("tunnel_medium_type", resp.TunnelMediumType)
d.Set("network_id", resp.NetworkID)
return nil
}
func resourceAccountGetResourceData(d *schema.ResourceData) (*unifi.Account, error) {
return &unifi.Account{
Name: d.Get("name").(string),
XPassword: d.Get("password").(string),
TunnelType: d.Get("tunnel_type").(int),
TunnelMediumType: d.Get("tunnel_medium_type").(int),
NetworkID: d.Get("network_id").(string),
}, nil
}

View File

@@ -0,0 +1,54 @@
package provider
import (
"fmt"
"testing"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)
func TestAccAccount_basic(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { preCheck(t) },
ProviderFactories: providerFactories,
// TODO: CheckDestroy: ,
Steps: []resource.TestStep{
{
Config: testAccAccountConfig("tfacc", "secure"),
Check: resource.ComposeTestCheckFunc(
// testCheckNetworkExists(t, "name"),
resource.TestCheckResourceAttr("unifi_account.test", "name", "tfacc"),
),
},
importStep("unifi_account.test"),
},
})
}
func TestAccAccount_mac(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { preCheck(t) },
ProviderFactories: providerFactories,
// TODO: CheckDestroy: ,
Steps: []resource.TestStep{
{
Config: testAccAccountConfig("00B0D06FC226", "00B0D06FC226"),
Check: resource.ComposeTestCheckFunc(
// testCheckNetworkExists(t, "name"),
resource.TestCheckResourceAttr("unifi_account.test", "name", "00B0D06FC226"),
resource.TestCheckResourceAttr("unifi_account.test", "password", "00B0D06FC226"),
),
},
importStep("unifi_account.test"),
},
})
}
func testAccAccountConfig(name, password string) string {
return fmt.Sprintf(`
resource "unifi_account" "test" {
name = "%s"
password = "%s"
}
`, name, password)
}

View File

@@ -13,7 +13,7 @@ import (
func resourceRadiusProfile() *schema.Resource {
return &schema.Resource{
Description: "`unifi_radius_profile` manages radius profiles.",
Description: "`unifi_radius_profile` manages RADIUS profiles.",
CreateContext: resourceRadiusProfileCreate,
ReadContext: resourceRadiusProfileRead,
@@ -42,7 +42,7 @@ func resourceRadiusProfile() *schema.Resource {
Required: true,
},
"accounting_enabled": {
Description: "Specifies whether to use radius accounting.",
Description: "Specifies whether to use RADIUS accounting.",
Type: schema.TypeBool,
Default: false,
Optional: true,
@@ -60,13 +60,13 @@ func resourceRadiusProfile() *schema.Resource {
Optional: true,
},
"use_usg_acct_server": {
Description: "Specifies whether to use usg as a radius accounting server.",
Description: "Specifies whether to use usg as a RADIUS accounting server.",
Type: schema.TypeBool,
Default: false,
Optional: true,
},
"use_usg_auth_server": {
Description: "Specifies whether to use usg as a radius authentication server.",
Description: "Specifies whether to use usg as a RADIUS authentication server.",
Type: schema.TypeBool,
Default: false,
Optional: true,
@@ -405,12 +405,12 @@ func getRadiusProfileIDByName(ctx context.Context, client unifiClient, profileNa
continue
}
if idMatchingName != "" {
return "", fmt.Errorf("Found multiple radius profiles with name '%s'", profileName)
return "", fmt.Errorf("Found multiple RADIUS profiles with name '%s'", profileName)
}
idMatchingName = profile.ID
}
if idMatchingName == "" {
return "", fmt.Errorf("Found no radius profile with name '%s', found: %s", profileName, strings.Join(allNames, ", "))
return "", fmt.Errorf("Found no RADIUS profile with name '%s', found: %s", profileName, strings.Join(allNames, ", "))
}
return idMatchingName, nil
}