diff --git a/docs/resources/radius_profile.md b/docs/resources/radius_profile.md
new file mode 100644
index 0000000..164bbca
--- /dev/null
+++ b/docs/resources/radius_profile.md
@@ -0,0 +1,64 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "unifi_radius_profile Resource - terraform-provider-unifi"
+subcategory: ""
+description: |-
+ unifi_radius_profile manages radius profiles.
+---
+
+# unifi_radius_profile (Resource)
+
+`unifi_radius_profile` manages radius profiles.
+
+
+
+
+## Schema
+
+### Required
+
+- `name` (String) The name of the profile.
+
+### Optional
+
+- `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`.
+- `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 ``.
+
+### Read-Only
+
+- `id` (String) The ID of the settings.
+
+
+### Nested Schema for `acct_server`
+
+Required:
+
+- `ip` (String) IP address of accounting service server.
+- `xsecret` (String, Sensitive) RADIUS secret.
+
+Optional:
+
+- `port` (Number) Port of accounting service. Defaults to `1813`.
+
+
+
+### Nested Schema for `auth_server`
+
+Required:
+
+- `ip` (String) IP address of authentication service server.
+- `xsecret` (String, Sensitive) RADIUS secret.
+
+Optional:
+
+- `port` (Number) Port of authentication service. Defaults to `1812`.
+
+
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index f131f11..1c4ba25 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -90,6 +90,7 @@ func New(version string) func() *schema.Provider {
"unifi_user_group": resourceUserGroup(),
"unifi_user": resourceUser(),
"unifi_wlan": resourceWLAN(),
+ "unifi_radius_profile": resourceRadiusProfile(),
"unifi_setting_mgmt": resourceSettingMgmt(),
"unifi_setting_usg": resourceSettingUsg(),
diff --git a/internal/provider/resource_radius_profile.go b/internal/provider/resource_radius_profile.go
new file mode 100644
index 0000000..a116e0d
--- /dev/null
+++ b/internal/provider/resource_radius_profile.go
@@ -0,0 +1,415 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+ "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"
+ "strings"
+)
+
+func resourceRadiusProfile() *schema.Resource {
+ return &schema.Resource{
+ Description: "`unifi_radius_profile` manages radius profiles.",
+
+ CreateContext: resourceRadiusProfileCreate,
+ ReadContext: resourceRadiusProfileRead,
+ UpdateContext: resourceRadiusProfileUpdate,
+ DeleteContext: resourceRadiusProfileDelete,
+ Importer: &schema.ResourceImporter{
+ StateContext: importRadiusProfile,
+ },
+
+ Schema: map[string]*schema.Schema{
+ "id": {
+ Description: "The ID of the settings.",
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "site": {
+ Description: "The name of the site to associate the settings with.",
+ Type: schema.TypeString,
+ Computed: true,
+ Optional: true,
+ ForceNew: true,
+ },
+ "name": {
+ Description: "The name of the profile.",
+ Type: schema.TypeString,
+ Required: true,
+ },
+ "accounting_enabled": {
+ Description: "Specifies whether to use radius accounting.",
+ Type: schema.TypeBool,
+ Default: false,
+ Optional: true,
+ },
+ "interim_update_enabled": {
+ Description: "Specifies whether to use interim_update.",
+ Type: schema.TypeBool,
+ Default: false,
+ Optional: true,
+ },
+ "interim_update_interval": {
+ Description: "Specifies interim_update interval.",
+ Type: schema.TypeInt,
+ Default: 3600,
+ Optional: true,
+ },
+ "use_usg_acct_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.",
+ Type: schema.TypeBool,
+ Default: false,
+ Optional: true,
+ },
+ "vlan_enabled": {
+ Description: "Specifies whether to use vlan on wired connections.",
+ Type: schema.TypeBool,
+ Default: false,
+ Optional: true,
+ },
+ "vlan_wlan_mode": {
+ Description: "Specifies whether to use vlan on wireless connections. Must be one of `disabled`, `optional`, or `required`.",
+ Type: schema.TypeString,
+ Default: "",
+ Optional: true,
+ ValidateFunc: validation.StringInSlice([]string{"disabled", "optional", "required"}, false),
+ },
+ "auth_server": {
+ Description: "RADIUS authentication servers.",
+ Type: schema.TypeList,
+ Optional: true,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "ip": {
+ Description: "IP address of authentication service server.",
+ Type: schema.TypeString,
+ Required: true,
+ ValidateFunc: validation.IsIPAddress,
+ },
+ "port": {
+ Description: "Port of authentication service.",
+ Type: schema.TypeInt,
+ Optional: true,
+ Default: 1812,
+ ValidateFunc: validation.IsPortNumber,
+ },
+ "xsecret": {
+ Description: "RADIUS secret.",
+ Type: schema.TypeString,
+ Required: true,
+ Sensitive: true,
+ },
+ },
+ },
+ },
+ "acct_server": {
+ Description: "RADIUS accounting servers.",
+ Type: schema.TypeList,
+ Optional: true,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "ip": {
+ Description: "IP address of accounting service server.",
+ Type: schema.TypeString,
+ Required: true,
+ ValidateFunc: validation.IsIPAddress,
+ },
+ "port": {
+ Description: "Port of accounting service.",
+ Type: schema.TypeInt,
+ Optional: true,
+ Default: 1813,
+ ValidateFunc: validation.IsPortNumber,
+ },
+ "xsecret": {
+ Description: "RADIUS secret.",
+ Type: schema.TypeString,
+ Required: true,
+ Sensitive: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func setToAuthServers(set []interface{}) ([]unifi.RADIUSProfileAuthServers, error) {
+ var authServers []unifi.RADIUSProfileAuthServers
+ for _, item := range set {
+ data, ok := item.(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("unexpected data in block")
+ }
+ authServer, err := toAuthServer(data)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create port override: %w", err)
+ }
+ authServers = append(authServers, authServer)
+ }
+ return authServers, nil
+}
+
+func setToAcctServers(set []interface{}) ([]unifi.RADIUSProfileAcctServers, error) {
+ var acctServers []unifi.RADIUSProfileAcctServers
+ for _, item := range set {
+ data, ok := item.(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("unexpected data in block")
+ }
+ accServer, err := toAcctServer(data)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create port override: %w", err)
+ }
+ acctServers = append(acctServers, accServer)
+ }
+ return acctServers, nil
+}
+
+func toAuthServer(data map[string]interface{}) (unifi.RADIUSProfileAuthServers, error) {
+ return unifi.RADIUSProfileAuthServers{
+ IP: data["ip"].(string),
+ Port: data["port"].(int),
+ XSecret: data["xsecret"].(string),
+ }, nil
+}
+
+func toAcctServer(data map[string]interface{}) (unifi.RADIUSProfileAcctServers, error) {
+ return unifi.RADIUSProfileAcctServers{
+ IP: data["ip"].(string),
+ Port: data["port"].(int),
+ XSecret: data["xsecret"].(string),
+ }, nil
+}
+
+func setFromAuthServers(authServers []unifi.RADIUSProfileAuthServers) ([]map[string]interface{}, error) {
+ list := make([]map[string]interface{}, 0, len(authServers))
+ for _, authServer := range authServers {
+ v, err := fromAuthServer(authServer)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse ssh key: %w", err)
+ }
+ list = append(list, v)
+ }
+ return list, nil
+}
+
+func setFromAcctServers(acctServers []unifi.RADIUSProfileAcctServers) ([]map[string]interface{}, error) {
+ list := make([]map[string]interface{}, 0, len(acctServers))
+ for _, acctServer := range acctServers {
+ v, err := fromAcctServer(acctServer)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse ssh key: %w", err)
+ }
+ list = append(list, v)
+ }
+ return list, nil
+}
+
+func fromAuthServer(sshKey unifi.RADIUSProfileAuthServers) (map[string]interface{}, error) {
+ return map[string]interface{}{
+ "ip": sshKey.IP,
+ "port": sshKey.Port,
+ "xsecret": sshKey.XSecret,
+ }, nil
+}
+
+func fromAcctServer(sshKey unifi.RADIUSProfileAcctServers) (map[string]interface{}, error) {
+ return map[string]interface{}{
+ "ip": sshKey.IP,
+ "port": sshKey.Port,
+ "xsecret": sshKey.XSecret,
+ }, nil
+}
+
+func resourceRadiusProfileCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ c := meta.(*client)
+ req, err := resourceRadiusProfileGetResourceData(d)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ site := d.Get("site").(string)
+ if site == "" {
+ site = c.site
+ }
+ resp, err := c.c.CreateRADIUSProfile(ctx, site, req)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ d.SetId(resp.ID)
+
+ return resourceRadiusProfileSetResourceData(resp, d, site)
+}
+
+func resourceRadiusProfileGetResourceData(d *schema.ResourceData) (*unifi.RADIUSProfile, error) {
+ authServers, err := setToAuthServers(d.Get("auth_server").([]interface{}))
+ if err != nil {
+ return nil, fmt.Errorf("unable to auth_server ssh_key block: %w", err)
+ }
+ acctServers, err := setToAcctServers(d.Get("acct_server").([]interface{}))
+ if err != nil {
+ return nil, fmt.Errorf("unable to acct_server ssh_key block: %w", err)
+ }
+ return &unifi.RADIUSProfile{
+ Name: d.Get("name").(string),
+ InterimUpdateEnabled: d.Get("interim_update_enabled").(bool),
+ InterimUpdateInterval: d.Get("interim_update_interval").(int),
+ AccountingEnabled: d.Get("accounting_enabled").(bool),
+ UseUsgAcctServer: d.Get("use_usg_acct_server").(bool),
+ UseUsgAuthServer: d.Get("use_usg_auth_server").(bool),
+ VLANEnabled: d.Get("vlan_enabled").(bool),
+ VLANWLANMode: d.Get("vlan_wlan_mode").(string),
+ AuthServers: authServers,
+ AcctServers: acctServers,
+ }, nil
+}
+
+func resourceRadiusProfileSetResourceData(resp *unifi.RADIUSProfile, d *schema.ResourceData, site string) diag.Diagnostics {
+ authServers, err := setFromAuthServers(resp.AuthServers)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ acctServers, err := setFromAcctServers(resp.AcctServers)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ d.Set("site", site)
+ d.Set("name", resp.Name)
+
+ d.Set("interim_update_enabled", resp.InterimUpdateEnabled)
+ d.Set("interim_update_interval", resp.InterimUpdateInterval)
+ d.Set("accounting_enabled", resp.AccountingEnabled)
+ d.Set("use_usg_acct_server", resp.UseUsgAcctServer)
+ d.Set("use_usg_auth_server", resp.UseUsgAuthServer)
+ d.Set("vlan_enabled", resp.VLANEnabled)
+ d.Set("vlan_wlan_mode", resp.VLANWLANMode)
+ d.Set("auth_server", authServers)
+ d.Set("acct_server", acctServers)
+ return nil
+}
+
+func resourceRadiusProfileRead(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.GetRADIUSProfile(ctx, site, id)
+ if _, ok := err.(*unifi.NotFoundError); ok {
+ d.SetId("")
+ return nil
+ }
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ return resourceRadiusProfileSetResourceData(resp, d, site)
+}
+
+func resourceRadiusProfileUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ c := meta.(*client)
+
+ req, err := resourceRadiusProfileGetResourceData(d)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ req.ID = d.Id()
+
+ site := d.Get("site").(string)
+ if site == "" {
+ site = c.site
+ }
+ req.SiteID = site
+
+ resp, err := c.c.UpdateRADIUSProfile(ctx, site, req)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ return resourceRadiusProfileSetResourceData(resp, d, site)
+}
+
+func resourceRadiusProfileDelete(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
+ }
+
+ err := c.c.DeleteRADIUSProfile(ctx, site, id)
+ return diag.FromErr(err)
+}
+
+func importRadiusProfile(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
+ c := meta.(*client)
+ id := d.Id()
+ site := d.Get("site").(string)
+ if site == "" {
+ site = c.site
+ }
+
+ if strings.Contains(id, ":") {
+ importParts := strings.SplitN(id, ":", 2)
+ site = importParts[0]
+ id = importParts[1]
+ }
+
+ if strings.HasPrefix(id, "name=") {
+ targetName := strings.TrimPrefix(id, "name=")
+ var err error
+ if id, err = getRadiusProfileIDByName(ctx, c.c, targetName, site); err != nil {
+ return nil, err
+ }
+ }
+
+ if id != "" {
+ d.SetId(id)
+ }
+ if site != "" {
+ d.Set("site", site)
+ }
+
+ return []*schema.ResourceData{d}, nil
+}
+
+func getRadiusProfileIDByName(ctx context.Context, client unifiClient, profileName, site string) (string, error) {
+ radiusProfiles, err := client.ListRADIUSProfile(ctx, site)
+ if err != nil {
+ return "", err
+ }
+
+ idMatchingName := ""
+ allNames := []string{}
+ for _, profile := range radiusProfiles {
+ allNames = append(allNames, profile.Name)
+ if profile.Name != profileName {
+ continue
+ }
+ if idMatchingName != "" {
+ 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 idMatchingName, nil
+}
diff --git a/internal/provider/resource_radius_profile_test.go b/internal/provider/resource_radius_profile_test.go
new file mode 100644
index 0000000..2d94d6c
--- /dev/null
+++ b/internal/provider/resource_radius_profile_test.go
@@ -0,0 +1,113 @@
+package provider
+
+import (
+ "fmt"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+ "testing"
+)
+
+func TestAccRadiusProfile_basic(t *testing.T) {
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { preCheck(t) },
+ ProviderFactories: providerFactories,
+ // TODO: CheckDestroy: ,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccRadiusProfileConfig("test"),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("unifi_radius_profile.test", "name", "test"),
+ ),
+ },
+ importStep("unifi_radius_profile.test"),
+ },
+ })
+}
+
+func TestAccRadiusProfile_servers(t *testing.T) {
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { preCheck(t) },
+ ProviderFactories: providerFactories,
+ // TODO: CheckDestroy: ,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccRadiusProfileConfigServer(),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("unifi_radius_profile.test", "name", "test"),
+ ),
+ },
+ importStep("unifi_radius_profile.test"),
+ },
+ })
+}
+
+func TestAccRadiusProfile_importByName(t *testing.T) {
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { preCheck(t) },
+ ProviderFactories: providerFactories,
+ Steps: []resource.TestStep{
+ // Apply and import network by name.
+ {
+ Config: testAccRadiusProfileImport(),
+ },
+ {
+ Config: testAccRadiusProfileImport(),
+ ResourceName: "unifi_radius_profile.test",
+ ImportState: true,
+ ImportStateVerify: true,
+ ImportStateId: "name=imported",
+ },
+ },
+ })
+}
+
+func testAccRadiusProfileConfigServer() string {
+ return `
+resource "unifi_radius_profile" "test" {
+ name = "test"
+ auth_server {
+ ip = "192.168.1.1"
+ xsecret = "securepw1"
+ }
+ auth_server {
+ ip = "192.168.10.1"
+ port = 8888
+ xsecret = "securepw2"
+ }
+ acct_server {
+ ip = "192.168.1.1"
+ xsecret = "securepw1"
+ }
+ acct_server {
+ ip = "192.168.10.1"
+ port = 9999
+ xsecret = "securepw2"
+ }
+ use_usg_acct_server = false
+ use_usg_auth_server = false
+}
+`
+}
+
+func testAccRadiusProfileConfig(name string) string {
+ return fmt.Sprintf(`
+resource "unifi_radius_profile" "test" {
+ name = "%[1]s"
+}
+`, name)
+}
+
+func testAccRadiusProfileImport() string {
+ return `
+resource "unifi_radius_profile" "test" {
+ name = "imported"
+ auth_server {
+ ip = "192.168.1.1"
+ port = 1812
+ xsecret = "securepw"
+ }
+ use_usg_auth_server = true
+ vlan_enabled = true
+ vlan_wlan_mode = "required"
+}
+`
+}