feat: support API Key authentication to UniFi controller (#22)
This commit is contained in:
committed by
GitHub
parent
f5bd8ebb15
commit
b7fe359f6c
70
.editorconfig
Normal file
70
.editorconfig
Normal 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
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,3 +5,6 @@ dist
|
|||||||
terraform-provider-unifi
|
terraform-provider-unifi
|
||||||
|
|
||||||
vendor/
|
vendor/
|
||||||
|
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
@@ -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`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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/"
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ variable "username" {
|
|||||||
variable "password" {
|
variable "password" {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "api_key" {
|
||||||
|
}
|
||||||
|
|
||||||
variable "api_url" {
|
variable "api_url" {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user