diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..77d09cc --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore index 512fae1..dcfe6bd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ dist terraform-provider-unifi -vendor/ \ No newline at end of file +vendor/ + +.idea +*.iml \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 21f687e..f91111b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 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 ```terraform provider "unifi" { + api_key = var.api_key # optionally use UNIFI_API_KEY env var username = var.username # optionally use UNIFI_USERNAME env var password = var.password # optionally use UNIFI_PASSWORD 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 ### 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. +- `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. - `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` diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index 2039fea..ae86853 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -1,7 +1,8 @@ provider "unifi" { username = var.username # optionally use UNIFI_USERNAME 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 # certificates for your controller diff --git a/examples/provider/test.auto.tfvars b/examples/provider/test.auto.tfvars index 2992b73..33227c4 100644 --- a/examples/provider/test.auto.tfvars +++ b/examples/provider/test.auto.tfvars @@ -1,5 +1,6 @@ username = "tfacctest" password = "tfacctest1234" +# api_key = "tfacctest1234" # this assumes the default port for acc testing api_url = "https://localhost:8443/api/" diff --git a/examples/provider/variables.tf b/examples/provider/variables.tf index 340d09d..e3ef94a 100644 --- a/examples/provider/variables.tf +++ b/examples/provider/variables.tf @@ -4,6 +4,9 @@ variable "username" { variable "password" { } +variable "api_key" { +} + variable "api_url" { } diff --git a/internal/provider/controller_versions.go b/internal/provider/controller_versions.go index c28836b..abaa70b 100644 --- a/internal/provider/controller_versions.go +++ b/internal/provider/controller_versions.go @@ -6,16 +6,33 @@ import ( "github.com/hashicorp/go-version" ) +func asVersion(versionString string) *version.Version { + return version.Must(version.NewVersion(versionString)) +} + var ( - controllerV6 = version.Must(version.NewVersion("6.0.0")) - controllerV7 = version.Must(version.NewVersion("7.0.0")) + controllerV6 = asVersion("6.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 - controllerVersionWPA3 = version.Must(version.NewVersion("6.1.61")) + controllerVersionWPA3 = asVersion("6.1.61") ) -func (c *client) ControllerVersion() *version.Version { - return version.Must(version.NewVersion(c.c.Version())) +func (c *client) IsControllerV6() bool { + 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 { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f574af0..188ad6c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "errors" "fmt" + "github.com/hashicorp/go-version" "log" "net" "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` " + "environment variable.", Type: schema.TypeString, - Required: true, + Optional: true, DefaultFunc: schema.EnvDefaultFunc("UNIFI_USERNAME", ""), }, "password": { Description: "Password for the user accessing the API. Can be specified with the `UNIFI_PASSWORD` " + "environment variable.", Type: schema.TypeString, - Required: true, + Optional: true, 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": { 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 " + @@ -134,10 +142,16 @@ func createHTTPTransport(insecure bool, subsystem string) http.RoundTripper { 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) { user := d.Get("username").(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) site := d.Get("site").(string) insecure := d.Get("allow_insecure").(bool) @@ -145,6 +159,7 @@ func configure(version string, p *schema.Provider) schema.ConfigureContextFunc { URL: baseURL, User: user, Password: pass, + APIKey: apiKey, HttpRoundTripperProvider: func() http.RoundTripper { return createHTTPTransport(insecure, "unifi") }, @@ -161,8 +176,12 @@ func configure(version string, p *schema.Provider) schema.ConfigureContextFunc { return nil, diag.FromErr(err) } c := &client{ - c: unifiClient, - site: site, + c: unifiClient, + 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 @@ -191,6 +210,7 @@ func IsServerErrorContains(err error, messageContains string) bool { } type client struct { - c unifi.Client - site string + c unifi.Client + site string + version *version.Version } diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 3380097..2feeef0 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -30,7 +30,7 @@ var testClient unifi.Client func TestMain(m *testing.M) { if os.Getenv("TF_ACC") == "" { - // short circuit non acceptance test runs + // short circuit non-acceptance test runs os.Exit(m.Run()) } diff --git a/internal/provider/resource_setting_usg.go b/internal/provider/resource_setting_usg.go index 2cb7214..772c98f 100644 --- a/internal/provider/resource_setting_usg.go +++ b/internal/provider/resource_setting_usg.go @@ -77,8 +77,8 @@ func resourceSettingUsgUpdateResourceData(d *schema.ResourceData, meta interface //nolint // GetOkExists is deprecated, but using here: if mdns, hasMdns := d.GetOkExists("multicast_dns_enabled"); hasMdns { - if v := c.ControllerVersion(); v.GreaterThanOrEqual(controllerV7) { - return fmt.Errorf("multicast_dns_enabled is not supported on controller version %v", c.ControllerVersion()) + if c.IsControllerV7() { + return fmt.Errorf("multicast_dns_enabled is not supported on controller version %v", c.version) } setting.MdnsEnabled = mdns.(bool) diff --git a/internal/provider/resource_wlan.go b/internal/provider/resource_wlan.go index 5cc4a81..f408ebd 100644 --- a/internal/provider/resource_wlan.go +++ b/internal/provider/resource_wlan.go @@ -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") } } - if v := c.ControllerVersion(); v.LessThanOrEqual(controllerVersionWPA3) { + if !c.SupportsWPA3() { 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) } }