diff --git a/CHANGELOG.md b/CHANGELOG.md index f7ebdca1f..a14d2f7e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] - Add `headers` for the provider connection ([#1057](https://github.com/elastic/terraform-provider-elasticstack/pull/1057)) +- Migrate `elasticstack_elasticsearch_system_user` resource to Terraform plugin framework ([#1154](https://github.com/elastic/terraform-provider-elasticstack/pull/1154)) - Add custom `endpoint` configuration support for snapshot repository setup ([#1158](https://github.com/elastic/terraform-provider-elasticstack/pull/1158)) ## [0.11.15] - 2025-04-23 diff --git a/docs/resources/elasticsearch_security_system_user.md b/docs/resources/elasticsearch_security_system_user.md index 2d00da36d..27c9910bf 100644 --- a/docs/resources/elasticsearch_security_system_user.md +++ b/docs/resources/elasticsearch_security_system_user.md @@ -41,10 +41,10 @@ resource "elasticstack_elasticsearch_security_system_user" "kibana_system" { ### Optional -- `elasticsearch_connection` (Block List, Max: 1, Deprecated) Elasticsearch connection configuration block. This property will be removed in a future provider version. Configure the Elasticsearch connection via the provider configuration instead. (see [below for nested schema](#nestedblock--elasticsearch_connection)) +- `elasticsearch_connection` (Block List, Deprecated) Elasticsearch connection configuration block. (see [below for nested schema](#nestedblock--elasticsearch_connection)) - `enabled` (Boolean) Specifies whether the user is enabled. The default value is true. -- `password` (String, Sensitive) The user’s password. Passwords must be at least 6 characters long. -- `password_hash` (String, Sensitive) A hash of the user’s password. This must be produced using the same hashing algorithm as has been configured for password storage (see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-settings.html#hashing-settings). +- `password` (String, Sensitive) The user's password. Passwords must be at least 6 characters long. +- `password_hash` (String, Sensitive) A hash of the user's password. This must be produced using the same hashing algorithm as has been configured for password storage (see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-settings.html#hashing-settings). ### Read-Only diff --git a/go.mod b/go.mod index 6582a7e03..8edce32c5 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/agext/levenshtein v1.2.3 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect - github.com/cloudflare/circl v1.6.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.17.0 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -68,18 +68,18 @@ require ( github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/zclconf/go-cty v1.16.2 // indirect + github.com/zclconf/go-cty v1.16.3 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.34.0 // indirect go.opentelemetry.io/otel/trace v1.34.0 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/sync v0.14.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/tools v0.23.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.33.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/grpc v1.72.1 // indirect diff --git a/go.sum b/go.sum index 305fb7368..273421342 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmms github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= -github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= -github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -186,8 +186,8 @@ github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7V github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70= -github.com/zclconf/go-cty v1.16.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= +github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -206,22 +206,22 @@ go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -245,15 +245,15 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= diff --git a/internal/utils/testutils.go b/internal/acctest/checks/resource_list.go similarity index 96% rename from internal/utils/testutils.go rename to internal/acctest/checks/resource_list.go index c3f2ef1cc..c488c1677 100644 --- a/internal/utils/testutils.go +++ b/internal/acctest/checks/resource_list.go @@ -1,4 +1,4 @@ -package utils +package checks import ( "fmt" diff --git a/internal/acctest/checks/user_auth.go b/internal/acctest/checks/user_auth.go new file mode 100644 index 000000000..5200ebf0a --- /dev/null +++ b/internal/acctest/checks/user_auth.go @@ -0,0 +1,42 @@ +package checks + +import ( + "encoding/base64" + "fmt" + "io" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func CheckUserCanAuthenticate(username string, password string) func(*terraform.State) error { + return func(s *terraform.State) error { + client, err := clients.NewAcceptanceTestingClient() + if err != nil { + return err + } + + esClient, err := client.GetESClient() + if err != nil { + return err + } + + credentials := fmt.Sprintf("%s:%s", username, password) + authHeader := fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(credentials))) + + req := esClient.Security.Authenticate.WithHeader(map[string]string{"Authorization": authHeader}) + resp, err := esClient.Security.Authenticate(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + if resp.IsError() { + body, err := io.ReadAll(resp.Body) + + return fmt.Errorf("failed to authenticate as test user [%s] %s %s", username, body, err) + } + return nil + } +} diff --git a/internal/clients/elasticsearch/security.go b/internal/clients/elasticsearch/security.go index 118005492..6ce83aa09 100644 --- a/internal/clients/elasticsearch/security.go +++ b/internal/clients/elasticsearch/security.go @@ -89,49 +89,73 @@ func DeleteUser(ctx context.Context, apiClient *clients.ApiClient, username stri return diags } -func EnableUser(ctx context.Context, apiClient *clients.ApiClient, username string) diag.Diagnostics { - var diags diag.Diagnostics +func EnableUser(ctx context.Context, apiClient *clients.ApiClient, username string) fwdiag.Diagnostics { + var diags fwdiag.Diagnostics esClient, err := apiClient.GetESClient() if err != nil { - return diag.FromErr(err) + diags.AddError( + "Unable to get Elasticsearch client", + err.Error(), + ) + return diags } res, err := esClient.Security.EnableUser(username, esClient.Security.EnableUser.WithContext(ctx)) if err != nil { - return diag.FromErr(err) + diags.AddError( + "Unable to enable system user", + err.Error(), + ) + return diags } defer res.Body.Close() - if diags := utils.CheckError(res, "Unable to enable system user"); diags.HasError() { + if diags := utils.CheckErrorFromFW(res, "Unable to enable system user"); diags.HasError() { return diags } return diags } -func DisableUser(ctx context.Context, apiClient *clients.ApiClient, username string) diag.Diagnostics { - var diags diag.Diagnostics +func DisableUser(ctx context.Context, apiClient *clients.ApiClient, username string) fwdiag.Diagnostics { + var diags fwdiag.Diagnostics esClient, err := apiClient.GetESClient() if err != nil { - return diag.FromErr(err) + diags.AddError( + "Unable to get Elasticsearch client", + err.Error(), + ) + return diags } res, err := esClient.Security.DisableUser(username, esClient.Security.DisableUser.WithContext(ctx)) if err != nil { - return diag.FromErr(err) + diags.AddError( + "Unable to disable system user", + err.Error(), + ) + return diags } defer res.Body.Close() - if diags := utils.CheckError(res, "Unable to disable system user"); diags.HasError() { + if diags := utils.CheckErrorFromFW(res, "Unable to disable system user"); diags.HasError() { return diags } return diags } -func ChangeUserPassword(ctx context.Context, apiClient *clients.ApiClient, username string, userPassword *models.UserPassword) diag.Diagnostics { - var diags diag.Diagnostics +func ChangeUserPassword(ctx context.Context, apiClient *clients.ApiClient, username string, userPassword *models.UserPassword) fwdiag.Diagnostics { + var diags fwdiag.Diagnostics userPasswordBytes, err := json.Marshal(userPassword) if err != nil { - return diag.FromErr(err) + diags.AddError( + "Unable to marshal user password", + err.Error(), + ) + return diags } esClient, err := apiClient.GetESClient() if err != nil { - return diag.FromErr(err) + diags.AddError( + "Unable to get Elasticsearch client", + err.Error(), + ) + return diags } res, err := esClient.Security.ChangePassword( bytes.NewReader(userPasswordBytes), @@ -139,10 +163,14 @@ func ChangeUserPassword(ctx context.Context, apiClient *clients.ApiClient, usern esClient.Security.ChangePassword.WithContext(ctx), ) if err != nil { - return diag.FromErr(err) + diags.AddError( + "Unable to change user password", + err.Error(), + ) + return diags } defer res.Body.Close() - if diags := utils.CheckError(res, "Unable to change user's password"); diags.HasError() { + if diags := utils.CheckErrorFromFW(res, "Unable to change user's password"); diags.HasError() { return diags } return diags diff --git a/internal/elasticsearch/security/role_data_source_test.go b/internal/elasticsearch/security/role_data_source_test.go index 1fbebe705..aeccdf30e 100644 --- a/internal/elasticsearch/security/role_data_source_test.go +++ b/internal/elasticsearch/security/role_data_source_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/elastic/terraform-provider-elasticstack/internal/acctest" - "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/elastic/terraform-provider-elasticstack/internal/acctest/checks" "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -21,11 +21,11 @@ func TestAccDataSourceSecurityRole(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role.test", "name", "data_source_test"), resource.TestCheckTypeSetElemAttr("data.elasticstack_elasticsearch_security_role.test", "cluster.*", "all"), - utils.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role.test", "indices.0.names", []string{"index1", "index2"}), + checks.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role.test", "indices.0.names", []string{"index1", "index2"}), resource.TestCheckTypeSetElemAttr("data.elasticstack_elasticsearch_security_role.test", "indices.0.privileges.*", "all"), resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices", "true"), resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role.test", "applications.0.application", "myapp"), - utils.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role.test", "applications.0.privileges", []string{"admin", "read"}), + checks.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role.test", "applications.0.privileges", []string{"admin", "read"}), resource.TestCheckTypeSetElemAttr("data.elasticstack_elasticsearch_security_role.test", "applications.0.resources.*", "*"), resource.TestCheckTypeSetElemAttr("data.elasticstack_elasticsearch_security_role.test", "run_as.*", "other_user"), resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role.test", "metadata", `{"version":1}`), @@ -37,11 +37,11 @@ func TestAccDataSourceSecurityRole(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role.test", "name", "data_source_test"), resource.TestCheckTypeSetElemAttr("data.elasticstack_elasticsearch_security_role.test", "cluster.*", "all"), - utils.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role.test", "indices.0.names", []string{"index1", "index2"}), + checks.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role.test", "indices.0.names", []string{"index1", "index2"}), resource.TestCheckTypeSetElemAttr("data.elasticstack_elasticsearch_security_role.test", "indices.0.privileges.*", "all"), resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices", "true"), resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role.test", "applications.0.application", "myapp"), - utils.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role.test", "applications.0.privileges", []string{"admin", "read"}), + checks.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role.test", "applications.0.privileges", []string{"admin", "read"}), resource.TestCheckTypeSetElemAttr("data.elasticstack_elasticsearch_security_role.test", "applications.0.resources.*", "*"), resource.TestCheckTypeSetElemAttr("data.elasticstack_elasticsearch_security_role.test", "run_as.*", "other_user"), resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role.test", "metadata", `{"version":1}`), @@ -55,11 +55,11 @@ func TestAccDataSourceSecurityRole(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role.test", "name", "data_source_test"), resource.TestCheckTypeSetElemAttr("data.elasticstack_elasticsearch_security_role.test", "cluster.*", "all"), - utils.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role.test", "indices.0.names", []string{"index1", "index2"}), + checks.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role.test", "indices.0.names", []string{"index1", "index2"}), resource.TestCheckTypeSetElemAttr("data.elasticstack_elasticsearch_security_role.test", "indices.0.privileges.*", "all"), resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role.test", "indices.0.allow_restricted_indices", "true"), resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role.test", "applications.0.application", "myapp"), - utils.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role.test", "applications.0.privileges", []string{"admin", "read"}), + checks.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role.test", "applications.0.privileges", []string{"admin", "read"}), resource.TestCheckTypeSetElemAttr("data.elasticstack_elasticsearch_security_role.test", "applications.0.resources.*", "*"), resource.TestCheckTypeSetElemAttr("data.elasticstack_elasticsearch_security_role.test", "run_as.*", "other_user"), resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role.test", "metadata", `{"version":1}`), diff --git a/internal/elasticsearch/security/role_mapping_data_source_test.go b/internal/elasticsearch/security/role_mapping_data_source_test.go index 821df8146..154aec605 100644 --- a/internal/elasticsearch/security/role_mapping_data_source_test.go +++ b/internal/elasticsearch/security/role_mapping_data_source_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/elastic/terraform-provider-elasticstack/internal/acctest" - "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/elastic/terraform-provider-elasticstack/internal/acctest/checks" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -18,7 +18,7 @@ func TestAccDataSourceSecurityRoleMapping(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role_mapping.test", "name", "data_source_test"), resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role_mapping.test", "enabled", "true"), - utils.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role_mapping.test", "roles", []string{"admin"}), + checks.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role_mapping.test", "roles", []string{"admin"}), resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role_mapping.test", "rules", `{"any":[{"field":{"username":"esadmin"}},{"field":{"groups":"cn=admins,dc=example,dc=com"}}]}`), resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role_mapping.test", "metadata", `{"version":1}`), ), diff --git a/internal/elasticsearch/security/role_mapping_test.go b/internal/elasticsearch/security/role_mapping_test.go index c88b0df7c..dcde6ebff 100644 --- a/internal/elasticsearch/security/role_mapping_test.go +++ b/internal/elasticsearch/security/role_mapping_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + "github.com/elastic/terraform-provider-elasticstack/internal/acctest/checks" "github.com/elastic/terraform-provider-elasticstack/internal/clients" - "github.com/elastic/terraform-provider-elasticstack/internal/utils" sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" @@ -25,7 +25,7 @@ func TestResourceRoleMapping(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role_mapping.test", "name", roleMappingName), resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role_mapping.test", "enabled", "true"), - utils.TestCheckResourceListAttr("elasticstack_elasticsearch_security_role_mapping.test", "roles", []string{"admin"}), + checks.TestCheckResourceListAttr("elasticstack_elasticsearch_security_role_mapping.test", "roles", []string{"admin"}), resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role_mapping.test", "rules", `{"any":[{"field":{"username":"esadmin"}},{"field":{"groups":"cn=admins,dc=example,dc=com"}}]}`), resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role_mapping.test", "metadata", `{"version":1}`), ), @@ -35,7 +35,7 @@ func TestResourceRoleMapping(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role_mapping.test", "name", roleMappingName), resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role_mapping.test", "enabled", "false"), - utils.TestCheckResourceListAttr("elasticstack_elasticsearch_security_role_mapping.test", "roles", []string{"admin", "user"}), + checks.TestCheckResourceListAttr("elasticstack_elasticsearch_security_role_mapping.test", "roles", []string{"admin", "user"}), resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role_mapping.test", "rules", `{"any":[{"field":{"username":"esadmin"}},{"field":{"groups":"cn=admins,dc=example,dc=com"}}]}`), resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_role_mapping.test", "metadata", `{}`), ), diff --git a/internal/elasticsearch/security/system_user.go b/internal/elasticsearch/security/system_user.go deleted file mode 100644 index fc2b3cf2a..000000000 --- a/internal/elasticsearch/security/system_user.go +++ /dev/null @@ -1,161 +0,0 @@ -package security - -import ( - "context" - "fmt" - "regexp" - - "github.com/elastic/terraform-provider-elasticstack/internal/clients" - "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch" - "github.com/elastic/terraform-provider-elasticstack/internal/models" - "github.com/elastic/terraform-provider-elasticstack/internal/utils" - "github.com/hashicorp/terraform-plugin-log/tflog" - "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" -) - -func ResourceSystemUser() *schema.Resource { - userSchema := map[string]*schema.Schema{ - "id": { - Description: "Internal identifier of the resource", - Type: schema.TypeString, - Computed: true, - }, - "username": { - Description: "An identifier for the system user (see https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html).", - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.All( - validation.StringLenBetween(1, 1024), - validation.StringMatch(regexp.MustCompile(`^[[:graph:]]+$`), "must contain alphanumeric characters (a-z, A-Z, 0-9), spaces, punctuation, and printable symbols in the Basic Latin (ASCII) block. Leading or trailing whitespace is not allowed"), - ), - }, - "password": { - Description: "The user’s password. Passwords must be at least 6 characters long.", - Type: schema.TypeString, - Optional: true, - Sensitive: true, - ValidateFunc: validation.StringLenBetween(6, 128), - ConflictsWith: []string{"password_hash"}, - }, - "password_hash": { - Description: "A hash of the user’s password. This must be produced using the same hashing algorithm as has been configured for password storage (see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-settings.html#hashing-settings).", - Type: schema.TypeString, - Optional: true, - Sensitive: true, - ValidateFunc: validation.StringLenBetween(6, 128), - ConflictsWith: []string{"password"}, - }, - "enabled": { - Description: "Specifies whether the user is enabled. The default value is true.", - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - } - - utils.AddConnectionSchema(userSchema) - - return &schema.Resource{ - Description: "Updates system user's password and enablement. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html", - - CreateContext: resourceSecuritySystemUserPut, - UpdateContext: resourceSecuritySystemUserPut, - ReadContext: resourceSecuritySystemUserRead, - DeleteContext: resourceSecuritySystemUserDelete, - - Schema: userSchema, - } -} - -func resourceSecuritySystemUserPut(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, diags := clients.NewApiClientFromSDKResource(d, meta) - if diags.HasError() { - return diags - } - usernameId := d.Get("username").(string) - id, diags := client.ID(ctx, usernameId) - if diags.HasError() { - return diags - } - - user, diags := elasticsearch.GetUser(ctx, client, usernameId) - if diags.HasError() { - return diags - } - if user == nil || !user.IsSystemUser() { - return diag.Errorf(`System user "%s" not found`, usernameId) - } - - var userPassword models.UserPassword - if v, ok := d.GetOk("password"); ok && d.HasChange("password") { - password := v.(string) - userPassword.Password = &password - } - if v, ok := d.GetOk("password_hash"); ok && d.HasChange("password_hash") { - pass_hash := v.(string) - userPassword.PasswordHash = &pass_hash - } - if userPassword.Password != nil || userPassword.PasswordHash != nil { - if diags := elasticsearch.ChangeUserPassword(ctx, client, usernameId, &userPassword); diags.HasError() { - return diags - } - } - - if d.HasChange("enabled") { - if d.Get("enabled").(bool) { - if diags := elasticsearch.EnableUser(ctx, client, usernameId); diags.HasError() { - return diags - } - } else { - if diags := elasticsearch.DisableUser(ctx, client, usernameId); diags.HasError() { - return diags - } - } - } - - d.SetId(id.String()) - return resourceSecuritySystemUserRead(ctx, d, meta) -} - -func resourceSecuritySystemUserRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, diags := clients.NewApiClientFromSDKResource(d, meta) - if diags.HasError() { - return diags - } - compId, diags := clients.CompositeIdFromStr(d.Id()) - if diags.HasError() { - return diags - } - usernameId := compId.ResourceId - - user, diags := elasticsearch.GetUser(ctx, client, usernameId) - if diags == nil && (user == nil || !user.IsSystemUser()) { - tflog.Warn(ctx, fmt.Sprintf(`System user "%s" not found, removing from state`, compId.ResourceId)) - d.SetId("") - return diags - } - if diags.HasError() { - return diags - } - - if err := d.Set("username", usernameId); err != nil { - return diag.FromErr(err) - } - if err := d.Set("enabled", user.Enabled); err != nil { - return diag.FromErr(err) - } - - return diags -} - -func resourceSecuritySystemUserDelete(ctx context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - compId, diags := clients.CompositeIdFromStr(d.Id()) - if diags.HasError() { - return diags - } - tflog.Warn(ctx, fmt.Sprintf(`System user '%s' is not deletable, just removing from state`, compId.ResourceId)) - return nil -} diff --git a/internal/elasticsearch/security/system_user_test.go b/internal/elasticsearch/security/system_user/acc_test.go similarity index 58% rename from internal/elasticsearch/security/system_user_test.go rename to internal/elasticsearch/security/system_user/acc_test.go index fef75b269..60c33b09e 100644 --- a/internal/elasticsearch/security/system_user_test.go +++ b/internal/elasticsearch/security/system_user/acc_test.go @@ -1,10 +1,11 @@ -package security_test +package system_user_test import ( "regexp" "testing" "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + "github.com/elastic/terraform-provider-elasticstack/internal/acctest/checks" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -24,7 +25,8 @@ func TestAccResourceSecuritySystemUser(t *testing.T) { Config: testAccResourceSecuritySystemUserUpdate, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_system_user.remote_monitoring_user", "username", "remote_monitoring_user"), - resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_system_user.remote_monitoring_user", "enabled", "false"), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_system_user.remote_monitoring_user", "enabled", "true"), + checks.CheckUserCanAuthenticate("remote_monitoring_user", "new_password"), ), }, }, @@ -44,6 +46,36 @@ func TestAccResourceSecuritySystemUserNotFound(t *testing.T) { }) } +func TestAccResourceSecuritySystemUserFromSDK(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + Steps: []resource.TestStep{ + { + // Create the system user with the last provider version where the system user resource was built on the SDK + ExternalProviders: map[string]resource.ExternalProvider{ + "elasticstack": { + Source: "elastic/elasticstack", + VersionConstraint: "0.11.15", + }, + }, + Config: testAccResourceSecuritySystemUserCreate, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_system_user.remote_monitoring_user", "username", "remote_monitoring_user"), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_system_user.remote_monitoring_user", "enabled", "true"), + ), + }, + { + ProtoV6ProviderFactories: acctest.Providers, + Config: testAccResourceSecuritySystemUserCreate, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_system_user.remote_monitoring_user", "username", "remote_monitoring_user"), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_system_user.remote_monitoring_user", "enabled", "true"), + ), + }, + }, + }) +} + const testAccResourceSecuritySystemUserCreate = ` provider "elasticstack" { elasticsearch {} @@ -51,9 +83,9 @@ provider "elasticstack" { resource "elasticstack_elasticsearch_security_system_user" "remote_monitoring_user" { username = "remote_monitoring_user" - password = "new_password" } - ` +` + const testAccResourceSecuritySystemUserUpdate = ` provider "elasticstack" { elasticsearch {} @@ -62,7 +94,6 @@ provider "elasticstack" { resource "elasticstack_elasticsearch_security_system_user" "remote_monitoring_user" { username = "remote_monitoring_user" password = "new_password" - enabled = false } ` const testAccResourceSecuritySystemUserNotFound = ` diff --git a/internal/elasticsearch/security/system_user/create.go b/internal/elasticsearch/security/system_user/create.go new file mode 100644 index 000000000..5c40eb5d2 --- /dev/null +++ b/internal/elasticsearch/security/system_user/create.go @@ -0,0 +1,15 @@ +package system_user + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *systemUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + diags := r.update(ctx, req.Plan, &resp.State) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/elasticsearch/security/system_user/delete.go b/internal/elasticsearch/security/system_user/delete.go new file mode 100644 index 000000000..998ccde55 --- /dev/null +++ b/internal/elasticsearch/security/system_user/delete.go @@ -0,0 +1,25 @@ +package system_user + +import ( + "context" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +func (r *systemUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data SystemUserData + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + compId, diags := clients.CompositeIdFromStrFw(data.Id.ValueString()) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Warn(ctx, fmt.Sprintf(`System user '%s' is not deletable, just removing from state`, compId.ResourceId)) +} diff --git a/internal/elasticsearch/security/system_user/models.go b/internal/elasticsearch/security/system_user/models.go new file mode 100644 index 000000000..d754a0149 --- /dev/null +++ b/internal/elasticsearch/security/system_user/models.go @@ -0,0 +1,14 @@ +package system_user + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type SystemUserData struct { + Id types.String `tfsdk:"id"` + ElasticsearchConnection types.List `tfsdk:"elasticsearch_connection"` + Username types.String `tfsdk:"username"` + Password types.String `tfsdk:"password"` + PasswordHash types.String `tfsdk:"password_hash"` + Enabled types.Bool `tfsdk:"enabled"` +} diff --git a/internal/elasticsearch/security/system_user/read.go b/internal/elasticsearch/security/system_user/read.go new file mode 100644 index 000000000..530b9f040 --- /dev/null +++ b/internal/elasticsearch/security/system_user/read.go @@ -0,0 +1,51 @@ +package system_user + +import ( + "context" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +func (r *systemUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data SystemUserData + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + compId, diags := clients.CompositeIdFromStrFw(data.Id.ValueString()) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + usernameId := compId.ResourceId + + client, diags := clients.MaybeNewApiClientFromFrameworkResource(ctx, data.ElasticsearchConnection, r.client) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + user, sdkDiags := elasticsearch.GetUser(ctx, client, usernameId) + resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...) + if resp.Diagnostics.HasError() { + return + } + + if user == nil || !user.IsSystemUser() { + tflog.Warn(ctx, fmt.Sprintf(`System user "%s" not found, removing from state`, compId.ResourceId)) + resp.State.RemoveResource(ctx) + return + } + + data.Username = types.StringValue(usernameId) + data.Enabled = types.BoolValue(user.Enabled) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/elasticsearch/security/system_user/resource.go b/internal/elasticsearch/security/system_user/resource.go new file mode 100644 index 000000000..63ef38e36 --- /dev/null +++ b/internal/elasticsearch/security/system_user/resource.go @@ -0,0 +1,26 @@ +package system_user + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func NewSystemUserResource() resource.Resource { + return &systemUserResource{} +} + +type systemUserResource struct { + client *clients.ApiClient +} + +func (r *systemUserResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_elasticsearch_security_system_user" +} + +func (r *systemUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + client, diags := clients.ConvertProviderData(req.ProviderData) + resp.Diagnostics.Append(diags...) + r.client = client +} diff --git a/internal/elasticsearch/security/system_user/schema.go b/internal/elasticsearch/security/system_user/schema.go new file mode 100644 index 000000000..6b529eceb --- /dev/null +++ b/internal/elasticsearch/security/system_user/schema.go @@ -0,0 +1,71 @@ +package system_user + +import ( + "context" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + providerschema "github.com/elastic/terraform-provider-elasticstack/internal/schema" +) + +func (r *systemUserResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = GetSchema() +} + +func GetSchema() schema.Schema { + return schema.Schema{ + MarkdownDescription: "Updates system user's password and enablement. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html", + Blocks: map[string]schema.Block{ + "elasticsearch_connection": providerschema.GetEsFWConnectionBlock("elasticsearch_connection", false), + }, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Internal identifier of the resource", + Computed: true, + }, + "username": schema.StringAttribute{ + MarkdownDescription: "An identifier for the system user (see https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html).", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 1024), + stringvalidator.RegexMatches(regexp.MustCompile(`^[[:graph:]]+$`), "must contain alphanumeric characters (a-z, A-Z, 0-9), spaces, punctuation, and printable symbols in the Basic Latin (ASCII) block. Leading or trailing whitespace is not allowed"), + }, + }, + "password": schema.StringAttribute{ + MarkdownDescription: "The user's password. Passwords must be at least 6 characters long.", + Optional: true, + Sensitive: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(6, 128), + stringvalidator.ConflictsWith(path.MatchRoot("password_hash")), + }, + }, + "password_hash": schema.StringAttribute{ + MarkdownDescription: "A hash of the user's password. This must be produced using the same hashing algorithm as has been configured for password storage (see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-settings.html#hashing-settings).", + Optional: true, + Sensitive: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(6, 128), + stringvalidator.ConflictsWith(path.MatchRoot("password")), + }, + }, + "enabled": schema.BoolAttribute{ + MarkdownDescription: "Specifies whether the user is enabled. The default value is true.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + }, + } +} diff --git a/internal/elasticsearch/security/system_user/update.go b/internal/elasticsearch/security/system_user/update.go new file mode 100644 index 000000000..1537f2f87 --- /dev/null +++ b/internal/elasticsearch/security/system_user/update.go @@ -0,0 +1,84 @@ +package system_user + +import ( + "context" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch" + "github.com/elastic/terraform-provider-elasticstack/internal/models" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func (r *systemUserResource) update(ctx context.Context, plan tfsdk.Plan, state *tfsdk.State) diag.Diagnostics { + var data SystemUserData + var diags diag.Diagnostics + diags.Append(plan.Get(ctx, &data)...) + if diags.HasError() { + return diags + } + + usernameId := data.Username.ValueString() + id, sdkDiags := r.client.ID(ctx, usernameId) + diags.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...) + if diags.HasError() { + return diags + } + + client, diags := clients.MaybeNewApiClientFromFrameworkResource(ctx, data.ElasticsearchConnection, r.client) + diags.Append(diags...) + if diags.HasError() { + return diags + } + + user, sdkDiags := elasticsearch.GetUser(ctx, client, usernameId) + diags.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...) + if diags.HasError() { + return diags + } + if user == nil || !user.IsSystemUser() { + diags.AddError("", fmt.Sprintf(`System user "%s" not found`, usernameId)) + return diags + } + + var userPassword models.UserPassword + if utils.IsKnown(data.Password) && (user.Password == nil || data.Password.ValueString() != *user.Password) { + userPassword.Password = data.Password.ValueStringPointer() + } + if utils.IsKnown(data.PasswordHash) && (user.PasswordHash == nil || data.PasswordHash.ValueString() != *user.PasswordHash) { + userPassword.PasswordHash = data.PasswordHash.ValueStringPointer() + } + if userPassword.Password != nil || userPassword.PasswordHash != nil { + diags.Append(elasticsearch.ChangeUserPassword(ctx, r.client, usernameId, &userPassword)...) + if diags.HasError() { + return diags + } + } + + if utils.IsKnown(data.Enabled) && !data.Enabled.IsNull() && data.Enabled.ValueBool() != user.Enabled { + if data.Enabled.ValueBool() { + diags.Append(elasticsearch.EnableUser(ctx, r.client, usernameId)...) + } else { + diags.Append(elasticsearch.DisableUser(ctx, r.client, usernameId)...) + } + if diags.HasError() { + return diags + } + } + + data.Id = types.StringValue(id.String()) + diags.Append(state.Set(ctx, &data)...) + return diags +} + +func (r *systemUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + diags := r.update(ctx, req.Plan, &resp.State) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/elasticsearch/security/user_test.go b/internal/elasticsearch/security/user_test.go index 531b75a3b..9311d3ca1 100644 --- a/internal/elasticsearch/security/user_test.go +++ b/internal/elasticsearch/security/user_test.go @@ -2,13 +2,13 @@ package security_test import ( "context" - "encoding/base64" "fmt" "io" "strings" "testing" "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + "github.com/elastic/terraform-provider-elasticstack/internal/acctest/checks" "github.com/elastic/terraform-provider-elasticstack/internal/clients" sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -103,7 +103,7 @@ func TestAccImportedUserDoesNotResetPassword(t *testing.T) { return nil }, - Check: checkUserCanAuthenticate(username, initialPassword), + Check: checks.CheckUserCanAuthenticate(username, initialPassword), }, { Config: testAccResourceSecurityUserUpdateNoPassword(username), @@ -112,7 +112,7 @@ func TestAccImportedUserDoesNotResetPassword(t *testing.T) { resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_user.test", "roles.*", "kibana_admin"), resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_security_user.test", "password"), resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_user.test", "full_name", "Test User"), - checkUserCanAuthenticate(username, initialPassword), + checks.CheckUserCanAuthenticate(username, initialPassword), ), }, { @@ -153,45 +153,13 @@ func TestAccImportedUserDoesNotResetPassword(t *testing.T) { resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_user.test", "email", "test@example.com"), resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_user.test", "password"), resource.TestCheckTypeSetElemAttr("elasticstack_elasticsearch_security_user.test", "roles.*", "kibana_user"), - checkUserCanAuthenticate(username, userUpdatedPassword), + checks.CheckUserCanAuthenticate(username, userUpdatedPassword), ), }, }, }) } -func checkUserCanAuthenticate(username string, password string) func(*terraform.State) error { - return func(s *terraform.State) error { - client, err := clients.NewAcceptanceTestingClient() - if err != nil { - return err - } - - esClient, err := client.GetESClient() - if err != nil { - return err - } - - credentials := fmt.Sprintf("%s:%s", username, password) - authHeader := fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(credentials))) - - req := esClient.Security.Authenticate.WithHeader(map[string]string{"Authorization": authHeader}) - resp, err := esClient.Security.Authenticate(req) - if err != nil { - return err - } - - defer resp.Body.Close() - - if resp.IsError() { - body, err := io.ReadAll(resp.Body) - - return fmt.Errorf("failed to authenticate as test user [%s] %s %s", username, body, err) - } - return nil - } -} - func testAccResourceSecurityUserCreate(username string) string { return fmt.Sprintf(` provider "elasticstack" { diff --git a/internal/kibana/role_data_source_test.go b/internal/kibana/role_data_source_test.go index 4b3339858..d54414186 100644 --- a/internal/kibana/role_data_source_test.go +++ b/internal/kibana/role_data_source_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/elastic/terraform-provider-elasticstack/internal/acctest" - "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/elastic/terraform-provider-elasticstack/internal/acctest/checks" "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -22,9 +22,9 @@ func TestAccDataSourceKibanaSecurityRole(t *testing.T) { resource.TestCheckResourceAttr("data.elasticstack_kibana_security_role.test", "name", "data_source_test"), resource.TestCheckNoResourceAttr("data.elasticstack_kibana_security_role.test", "kibana.0.feature.#"), resource.TestCheckNoResourceAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.field_security.#"), - utils.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.run_as", []string{"elastic", "kibana"}), - utils.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "kibana.0.base", []string{"all"}), - utils.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), + checks.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.run_as", []string{"elastic", "kibana"}), + checks.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "kibana.0.base", []string{"all"}), + checks.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), ), }, { @@ -34,13 +34,13 @@ func TestAccDataSourceKibanaSecurityRole(t *testing.T) { resource.TestCheckResourceAttr("data.elasticstack_kibana_security_role.test", "name", "data_source_test2"), resource.TestCheckNoResourceAttr("data.elasticstack_kibana_security_role.test", "kibana.0.feature.#"), resource.TestCheckNoResourceAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.field_security.#"), - utils.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.run_as", []string{"elastic", "kibana"}), - utils.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "kibana.0.base", []string{"all"}), - utils.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), - utils.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.clusters", []string{"test-cluster"}), - utils.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.field_security.0.grant", []string{"sample"}), - utils.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.names", []string{"sample"}), - utils.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.privileges", []string{"create", "read", "write"}), + checks.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.run_as", []string{"elastic", "kibana"}), + checks.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "kibana.0.base", []string{"all"}), + checks.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), + checks.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.clusters", []string{"test-cluster"}), + checks.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.field_security.0.grant", []string{"sample"}), + checks.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.names", []string{"sample"}), + checks.TestCheckResourceListAttr("data.elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.privileges", []string{"create", "read", "write"}), ), }, }, diff --git a/internal/kibana/role_test.go b/internal/kibana/role_test.go index 8c05983f6..70669df32 100644 --- a/internal/kibana/role_test.go +++ b/internal/kibana/role_test.go @@ -5,8 +5,8 @@ import ( "testing" "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + "github.com/elastic/terraform-provider-elasticstack/internal/acctest/checks" "github.com/elastic/terraform-provider-elasticstack/internal/clients" - "github.com/elastic/terraform-provider-elasticstack/internal/utils" "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" "github.com/hashicorp/go-version" sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" @@ -31,10 +31,10 @@ func TestAccResourceKibanaSecurityRole(t *testing.T) { resource.TestCheckResourceAttr("elasticstack_kibana_security_role.test", "name", roleName), resource.TestCheckNoResourceAttr("elasticstack_kibana_security_role.test", "kibana.0.base.#"), resource.TestCheckNoResourceAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.run_as.#"), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.names", []string{"sample"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.field_security.0.grant", []string{"sample"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.feature.2.privileges", []string{"minimal_read", "store_search_session", "url_create"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.names", []string{"sample"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.field_security.0.grant", []string{"sample"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.feature.2.privileges", []string{"minimal_read", "store_search_session", "url_create"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), ), }, { @@ -43,9 +43,9 @@ func TestAccResourceKibanaSecurityRole(t *testing.T) { resource.TestCheckResourceAttr("elasticstack_kibana_security_role.test", "name", roleName), resource.TestCheckNoResourceAttr("elasticstack_kibana_security_role.test", "kibana.0.feature.#"), resource.TestCheckNoResourceAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.field_security.#"), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.run_as", []string{"elastic", "kibana"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.base", []string{"all"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.run_as", []string{"elastic", "kibana"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.base", []string{"all"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), ), }, { @@ -55,14 +55,14 @@ func TestAccResourceKibanaSecurityRole(t *testing.T) { resource.TestCheckResourceAttr("elasticstack_kibana_security_role.test", "name", roleNameRemoteIndices), resource.TestCheckNoResourceAttr("elasticstack_kibana_security_role.test", "kibana.0.base.#"), resource.TestCheckNoResourceAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.run_as.#"), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.names", []string{"sample"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.field_security.0.grant", []string{"sample"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.feature.2.privileges", []string{"minimal_read", "store_search_session", "url_create"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.clusters", []string{"test-cluster"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.field_security.0.grant", []string{"sample"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.names", []string{"sample"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.privileges", []string{"create", "read", "write"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.names", []string{"sample"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.field_security.0.grant", []string{"sample"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.feature.2.privileges", []string{"minimal_read", "store_search_session", "url_create"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.clusters", []string{"test-cluster"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.field_security.0.grant", []string{"sample"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.names", []string{"sample"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.privileges", []string{"create", "read", "write"}), ), }, { @@ -72,13 +72,13 @@ func TestAccResourceKibanaSecurityRole(t *testing.T) { resource.TestCheckResourceAttr("elasticstack_kibana_security_role.test", "name", roleNameRemoteIndices), resource.TestCheckNoResourceAttr("elasticstack_kibana_security_role.test", "kibana.0.feature.#"), resource.TestCheckNoResourceAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.indices.0.field_security.#"), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.run_as", []string{"elastic", "kibana"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.base", []string{"all"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.clusters", []string{"test-cluster2"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.field_security.0.grant", []string{"sample2"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.names", []string{"sample2"}), - utils.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.privileges", []string{"create", "read", "write"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.run_as", []string{"elastic", "kibana"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.base", []string{"all"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "kibana.0.spaces", []string{"default"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.clusters", []string{"test-cluster2"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.field_security.0.grant", []string{"sample2"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.names", []string{"sample2"}), + checks.TestCheckResourceListAttr("elasticstack_kibana_security_role.test", "elasticsearch.0.remote_indices.0.privileges", []string{"create", "read", "write"}), ), }, }, diff --git a/internal/utils/diag.go b/internal/utils/diag.go index 4a2b158b5..7e7011911 100644 --- a/internal/utils/diag.go +++ b/internal/utils/diag.go @@ -42,6 +42,21 @@ func CheckError(res *esapi.Response, errMsg string) sdkdiag.Diagnostics { return diags } +func CheckErrorFromFW(res *esapi.Response, errMsg string) fwdiag.Diagnostics { + var diags fwdiag.Diagnostics + + if res.IsError() { + body, err := io.ReadAll(res.Body) + if err != nil { + diags.AddError(errMsg, err.Error()) + return diags + } + diags.AddError(errMsg, fmt.Sprintf("Failed with: %s", body)) + return diags + } + return diags +} + func CheckHttpError(res *http.Response, errMsg string) sdkdiag.Diagnostics { var diags sdkdiag.Diagnostics diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go index 04752f129..07c1781c4 100644 --- a/provider/plugin_framework.go +++ b/provider/plugin_framework.go @@ -9,6 +9,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/index" "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/indices" "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/api_key" + "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/system_user" "github.com/elastic/terraform-provider-elasticstack/internal/fleet/agent_policy" "github.com/elastic/terraform-provider-elasticstack/internal/fleet/enrollment_tokens" "github.com/elastic/terraform-provider-elasticstack/internal/fleet/integration" @@ -100,5 +101,6 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { integration_policy.NewResource, output.NewResource, server_host.NewResource, + system_user.NewSystemUserResource, } } diff --git a/provider/provider.go b/provider/provider.go index 105ee5067..15e5a32b6 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -93,7 +93,6 @@ func New(version string) *schema.Provider { "elasticstack_elasticsearch_security_role": security.ResourceRole(), "elasticstack_elasticsearch_security_role_mapping": security.ResourceRoleMapping(), "elasticstack_elasticsearch_security_user": security.ResourceUser(), - "elasticstack_elasticsearch_security_system_user": security.ResourceSystemUser(), "elasticstack_elasticsearch_snapshot_lifecycle": cluster.ResourceSlm(), "elasticstack_elasticsearch_snapshot_repository": cluster.ResourceSnapshotRepository(), "elasticstack_elasticsearch_script": cluster.ResourceScript(),