feat: support API Key authentication to UniFi controller (#22)

This commit is contained in:
Mateusz Filipowicz
2025-02-23 19:51:18 +01:00
committed by GitHub
parent f5bd8ebb15
commit b7fe359f6c
11 changed files with 149 additions and 19 deletions

70
.editorconfig Normal file
View File

@@ -0,0 +1,70 @@
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = false
max_line_length = 200
tab_width = 4
# this affects things like 'throws' in method declaration in the new line etc
ij_continuation_indent_size = 8
ij_formatter_off_tag = @formatter:off
ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = false
ij_smart_tabs = false
ij_wrap_on_typing = false
ij_any_block_comment_add_space = true
[*.go]
indent_style = tab
[*.conf]
ij_continuation_indent_size = 2
[*.properties]
ij_properties_align_group_field_declarations = false
ij_properties_keep_blank_lines = false
ij_properties_key_value_delimiter = equals
ij_properties_spaces_around_key_value_delimiter = false
[.editorconfig]
max_line_length = 300
ij_editorconfig_align_group_field_declarations = false
ij_editorconfig_space_after_colon = false
ij_editorconfig_space_after_comma = true
ij_editorconfig_space_before_colon = false
ij_editorconfig_space_before_comma = false
ij_editorconfig_spaces_around_assignment_operators = true
[BUCK]
indent_size = 4
tab_width = 4
ij_buck_keep_indents_on_empty_lines = false
[{*.bash,*.sh,*.zsh}]
ij_shell_binary_ops_start_line = false
ij_shell_keep_column_alignment_padding = false
ij_shell_minify_program = false
ij_shell_redirect_followed_by_space = false
ij_shell_switch_cases_indented = false
[{*.har,*.json}]
ij_json_keep_blank_lines_in_code = 0
ij_json_keep_indents_on_empty_lines = false
ij_json_keep_line_breaks = true
ij_json_space_after_colon = true
ij_json_space_after_comma = true
ij_json_space_before_colon = true
ij_json_space_before_comma = false
ij_json_spaces_within_braces = false
ij_json_spaces_within_brackets = false
ij_json_wrap_long_lines = false
[{*.yaml,*.yml}]
indent_size = 2
tab_width = 2
ij_yaml_keep_indents_on_empty_lines = false
ij_yaml_keep_line_breaks = true
ij_yaml_space_before_colon = false
ij_yaml_spaces_within_braces = true
ij_yaml_spaces_within_brackets = true

5
.gitignore vendored
View File

@@ -4,4 +4,7 @@
dist dist
terraform-provider-unifi terraform-provider-unifi
vendor/ vendor/
.idea
*.iml

View File

@@ -13,10 +13,13 @@ It is not recommended to use your own account for management of your controller.
Terraform is recommended. You can create a **Limited Admin** with **Local Access Only** and Terraform is recommended. You can create a **Limited Admin** with **Local Access Only** and
provide that information for authentication. Two-factor authentication is not supported in the provider. provide that information for authentication. Two-factor authentication is not supported in the provider.
It is recommended to use API Key authentication, if you are on controller version 9.0.108 or higher.
## Example Usage ## Example Usage
```terraform ```terraform
provider "unifi" { provider "unifi" {
api_key = var.api_key # optionally use UNIFI_API_KEY env var
username = var.username # optionally use UNIFI_USERNAME env var username = var.username # optionally use UNIFI_USERNAME env var
password = var.password # optionally use UNIFI_PASSWORD env var password = var.password # optionally use UNIFI_PASSWORD env var
api_url = var.api_url # optionally use UNIFI_API env var api_url = var.api_url # optionally use UNIFI_API env var
@@ -30,12 +33,24 @@ provider "unifi" {
} }
``` ```
### Obtaining an API Key
1. Open your Site in UniFi Site Manager
2. Click on Control Plane -> Admins & Users.
3. Select your Admin user.
4. Click Create API Key.
5. Add a name for your API Key.
6. Copy the key and store it securely, as it will only be displayed once.
7. Click Done to ensure the key is hashed and securely stored.
8. Use the API Key 🎉
<!-- schema generated by tfplugindocs --> <!-- schema generated by tfplugindocs -->
## Schema ## Schema
### Optional ### Optional
- `allow_insecure` (Boolean) Skip verification of TLS certificates of API requests. You may need to set this to `true` if you are using your local API without setting up a signed certificate. Can be specified with the `UNIFI_INSECURE` environment variable. - `allow_insecure` (Boolean) Skip verification of TLS certificates of API requests. You may need to set this to `true` if you are using your local API without setting up a signed certificate. Can be specified with the `UNIFI_INSECURE` environment variable.
- `api_key` (String) API key for the user accessing the API. Can be specified with the `UNIFI_API_KEY` environment variable. Requires controller version 9.0.108 or higher and `username` and `password` to be empty
- `api_url` (String) URL of the controller API. Can be specified with the `UNIFI_API` environment variable. You should **NOT** supply the path (`/api`), the SDK will discover the appropriate paths. This is to support UDM Pro style API paths as well as more standard controller paths. - `api_url` (String) URL of the controller API. Can be specified with the `UNIFI_API` environment variable. You should **NOT** supply the path (`/api`), the SDK will discover the appropriate paths. This is to support UDM Pro style API paths as well as more standard controller paths.
- `password` (String) Password for the user accessing the API. Can be specified with the `UNIFI_PASSWORD` environment variable. - `password` (String) Password for the user accessing the API. Can be specified with the `UNIFI_PASSWORD` environment variable.
- `site` (String) The site in the Unifi controller this provider will manage. Can be specified with the `UNIFI_SITE` environment variable. Default: `default` - `site` (String) The site in the Unifi controller this provider will manage. Can be specified with the `UNIFI_SITE` environment variable. Default: `default`

View File

@@ -1,7 +1,8 @@
provider "unifi" { provider "unifi" {
username = var.username # optionally use UNIFI_USERNAME env var username = var.username # optionally use UNIFI_USERNAME env var
password = var.password # optionally use UNIFI_PASSWORD env var password = var.password # optionally use UNIFI_PASSWORD env var
api_url = var.api_url # optionally use UNIFI_API env var api_key = var.api_key # optionally use UNIFI_API_KEY env var
api_url = var.api_url # optionally use UNIFI_API env var
# you may need to allow insecure TLS communications unless you have configured # you may need to allow insecure TLS communications unless you have configured
# certificates for your controller # certificates for your controller

View File

@@ -1,5 +1,6 @@
username = "tfacctest" username = "tfacctest"
password = "tfacctest1234" password = "tfacctest1234"
# api_key = "tfacctest1234"
# this assumes the default port for acc testing # this assumes the default port for acc testing
api_url = "https://localhost:8443/api/" api_url = "https://localhost:8443/api/"

View File

@@ -4,6 +4,9 @@ variable "username" {
variable "password" { variable "password" {
} }
variable "api_key" {
}
variable "api_url" { variable "api_url" {
} }

View File

@@ -6,16 +6,33 @@ import (
"github.com/hashicorp/go-version" "github.com/hashicorp/go-version"
) )
func asVersion(versionString string) *version.Version {
return version.Must(version.NewVersion(versionString))
}
var ( var (
controllerV6 = version.Must(version.NewVersion("6.0.0")) controllerV6 = asVersion("6.0.0")
controllerV7 = version.Must(version.NewVersion("7.0.0")) controllerV7 = asVersion("7.0.0")
controllerVersionApiKeyAuth = asVersion("9.0.108")
// https://community.ui.com/releases/UniFi-Network-Controller-6-1-61/62f1ad38-1ac5-430c-94b0-becbb8f71d7d // https://community.ui.com/releases/UniFi-Network-Controller-6-1-61/62f1ad38-1ac5-430c-94b0-becbb8f71d7d
controllerVersionWPA3 = version.Must(version.NewVersion("6.1.61")) controllerVersionWPA3 = asVersion("6.1.61")
) )
func (c *client) ControllerVersion() *version.Version { func (c *client) IsControllerV6() bool {
return version.Must(version.NewVersion(c.c.Version())) return c.version.GreaterThanOrEqual(controllerV6)
}
func (c *client) IsControllerV7() bool {
return c.version.GreaterThanOrEqual(controllerV7)
}
func (c *client) SupportsApiKeyAuthentication() bool {
return c.version.GreaterThanOrEqual(controllerVersionApiKeyAuth)
}
func (c *client) SupportsWPA3() bool {
return c.version.GreaterThanOrEqual(controllerVersionWPA3)
} }
func checkMinimumControllerVersion(versionString string) error { func checkMinimumControllerVersion(versionString string) error {

View File

@@ -5,6 +5,7 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"github.com/hashicorp/go-version"
"log" "log"
"net" "net"
"net/http" "net/http"
@@ -40,16 +41,23 @@ func New(version string) func() *schema.Provider {
Description: "Local user name for the Unifi controller API. Can be specified with the `UNIFI_USERNAME` " + Description: "Local user name for the Unifi controller API. Can be specified with the `UNIFI_USERNAME` " +
"environment variable.", "environment variable.",
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Optional: true,
DefaultFunc: schema.EnvDefaultFunc("UNIFI_USERNAME", ""), DefaultFunc: schema.EnvDefaultFunc("UNIFI_USERNAME", ""),
}, },
"password": { "password": {
Description: "Password for the user accessing the API. Can be specified with the `UNIFI_PASSWORD` " + Description: "Password for the user accessing the API. Can be specified with the `UNIFI_PASSWORD` " +
"environment variable.", "environment variable.",
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Optional: true,
DefaultFunc: schema.EnvDefaultFunc("UNIFI_PASSWORD", ""), DefaultFunc: schema.EnvDefaultFunc("UNIFI_PASSWORD", ""),
}, },
"api_key": {
Description: "API Key for the user accessing the API. Can be specified with the `UNIFI_API_KEY` " +
"environment variable. Controller version 9.0.108 or later is required.",
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("UNIFI_API_KEY", ""),
},
"api_url": { "api_url": {
Description: "URL of the controller API. Can be specified with the `UNIFI_API` environment variable. " + Description: "URL of the controller API. Can be specified with the `UNIFI_API` environment variable. " +
"You should **NOT** supply the path (`/api`), the SDK will discover the appropriate paths. This is " + "You should **NOT** supply the path (`/api`), the SDK will discover the appropriate paths. This is " +
@@ -134,10 +142,16 @@ func createHTTPTransport(insecure bool, subsystem string) http.RoundTripper {
return t return t
} }
func configure(version string, p *schema.Provider) schema.ConfigureContextFunc { func configure(v string, p *schema.Provider) schema.ConfigureContextFunc {
return func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { return func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
user := d.Get("username").(string) user := d.Get("username").(string)
pass := d.Get("password").(string) pass := d.Get("password").(string)
apiKey := d.Get("api_key").(string)
if apiKey != "" && (user != "" || pass != "") {
return nil, diag.FromErr(errors.New("only one of `username`/`password` or `api_key` can be set"))
} else if apiKey == "" && (user == "" || pass == "") {
return nil, diag.FromErr(errors.New("either `username` and `password` or `api_key` must be set"))
}
baseURL := d.Get("api_url").(string) baseURL := d.Get("api_url").(string)
site := d.Get("site").(string) site := d.Get("site").(string)
insecure := d.Get("allow_insecure").(bool) insecure := d.Get("allow_insecure").(bool)
@@ -145,6 +159,7 @@ func configure(version string, p *schema.Provider) schema.ConfigureContextFunc {
URL: baseURL, URL: baseURL,
User: user, User: user,
Password: pass, Password: pass,
APIKey: apiKey,
HttpRoundTripperProvider: func() http.RoundTripper { HttpRoundTripperProvider: func() http.RoundTripper {
return createHTTPTransport(insecure, "unifi") return createHTTPTransport(insecure, "unifi")
}, },
@@ -161,8 +176,12 @@ func configure(version string, p *schema.Provider) schema.ConfigureContextFunc {
return nil, diag.FromErr(err) return nil, diag.FromErr(err)
} }
c := &client{ c := &client{
c: unifiClient, c: unifiClient,
site: site, site: site,
version: version.Must(version.NewVersion(unifiClient.Version())),
}
if apiKey != "" && !c.SupportsApiKeyAuthentication() {
return nil, diag.FromErr(fmt.Errorf("API key authentication is not supported on this controller version: %s, you must be on %s or higher", c.version, controllerVersionApiKeyAuth))
} }
return c, nil return c, nil
@@ -191,6 +210,7 @@ func IsServerErrorContains(err error, messageContains string) bool {
} }
type client struct { type client struct {
c unifi.Client c unifi.Client
site string site string
version *version.Version
} }

View File

@@ -30,7 +30,7 @@ var testClient unifi.Client
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
if os.Getenv("TF_ACC") == "" { if os.Getenv("TF_ACC") == "" {
// short circuit non acceptance test runs // short circuit non-acceptance test runs
os.Exit(m.Run()) os.Exit(m.Run())
} }

View File

@@ -77,8 +77,8 @@ func resourceSettingUsgUpdateResourceData(d *schema.ResourceData, meta interface
//nolint // GetOkExists is deprecated, but using here: //nolint // GetOkExists is deprecated, but using here:
if mdns, hasMdns := d.GetOkExists("multicast_dns_enabled"); hasMdns { if mdns, hasMdns := d.GetOkExists("multicast_dns_enabled"); hasMdns {
if v := c.ControllerVersion(); v.GreaterThanOrEqual(controllerV7) { if c.IsControllerV7() {
return fmt.Errorf("multicast_dns_enabled is not supported on controller version %v", c.ControllerVersion()) return fmt.Errorf("multicast_dns_enabled is not supported on controller version %v", c.version)
} }
setting.MdnsEnabled = mdns.(bool) setting.MdnsEnabled = mdns.(bool)

View File

@@ -263,9 +263,9 @@ func resourceWLANGetResourceData(d *schema.ResourceData, meta interface{}) (*uni
return nil, fmt.Errorf("wpa3_support and wpa3_transition are only valid for security type wpapsk") return nil, fmt.Errorf("wpa3_support and wpa3_transition are only valid for security type wpapsk")
} }
} }
if v := c.ControllerVersion(); v.LessThanOrEqual(controllerVersionWPA3) { if !c.SupportsWPA3() {
if wpa3 || wpa3Transition { if wpa3 || wpa3Transition {
return nil, fmt.Errorf("WPA 3 support is not available on controller version %q, you must be on %q or higher", v, controllerVersionWPA3) return nil, fmt.Errorf("WPA 3 support is not available on controller version %q, you must be on %q or higher", c.version, controllerVersionWPA3)
} }
} }