From 88e7eb13552f6f3ca6d52f14acbf4f06cfa6501b Mon Sep 17 00:00:00 2001 From: Sivaanand Murugesan Date: Thu, 26 Dec 2024 13:34:25 +0530 Subject: [PATCH] PLT-1523: Added password_policy support in terraform. (#555) * PLT-1523: Added password_policy support in terraform. * added validation * added import support * update sdk * updated sdk * reviewable --- docs/resources/password_policy.md | 66 +++++ .../spectrocloud_password_policy/providers.tf | 14 + .../spectrocloud_password_policy/resource.tf | 16 ++ .../terraform.template.tfvars | 4 + .../spectrocloud_password_policy/variables.tf | 18 ++ go.mod | 25 +- go.sum | 57 ++-- spectrocloud/cluster_common.go | 5 + spectrocloud/provider.go | 11 +- spectrocloud/resource_password_policy.go | 252 ++++++++++++++++++ spectrocloud/resource_password_policy_test.go | 187 +++++++++++++ templates/resources/password_policy.md.tmpl | 40 +++ 12 files changed, 651 insertions(+), 44 deletions(-) create mode 100644 docs/resources/password_policy.md create mode 100644 examples/resources/spectrocloud_password_policy/providers.tf create mode 100644 examples/resources/spectrocloud_password_policy/resource.tf create mode 100644 examples/resources/spectrocloud_password_policy/terraform.template.tfvars create mode 100644 examples/resources/spectrocloud_password_policy/variables.tf create mode 100644 spectrocloud/resource_password_policy.go create mode 100644 spectrocloud/resource_password_policy_test.go create mode 100644 templates/resources/password_policy.md.tmpl diff --git a/docs/resources/password_policy.md b/docs/resources/password_policy.md new file mode 100644 index 00000000..0e858487 --- /dev/null +++ b/docs/resources/password_policy.md @@ -0,0 +1,66 @@ +--- +page_title: "spectrocloud_password_policy Resource - terraform-provider-spectrocloud" +subcategory: "" +description: |- + +--- + +# spectrocloud_password_policy (Resource) + + + +You can learn more about managing password policy in Palette by reviewing the [Password Policy](https://docs.spectrocloud.com/enterprise-version/system-management/account-management/credentials/#password-requirements-and-security) guide. + +~> The password_policy resource enforces a password compliance policy. By default, a password policy is configured in Palette with default values. Users can update the password compliance settings as per their requirements. When a spectrocloud_password_policy resource is destroyed, the password policy will revert to the Palette default settings. + +## Example Usage + +An example of managing an password policy in Palette. + +```hcl +resource "spectrocloud_password_policy" "policy_regex" { + # password_regex = "*" + password_expiry_days = 123 + first_reminder_days = 5 + min_digits = 1 + min_lowercase_letters = 12 + min_password_length = 12 + min_special_characters = 1 + min_uppercase_letters = 1 +} + +## import existing password policy +#import { +# to = spectrocloud_password_policy.password_policy +# id = "{tenantUID}" // tenant-uid. +#} + +``` + + +## Schema + +### Optional + +- `first_reminder_days` (Number) The number of days before the password expiry to send the first reminder to the user. Default is `5` days before expiry. +- `min_digits` (Number) The minimum number of numeric digits (0-9) required in the password. Ensures that passwords contain numerical characters. Minimum length of digit should be `1`. +- `min_lowercase_letters` (Number) The minimum number of lowercase letters (a-z) required in the password. Ensures that lowercase characters are included for password complexity. Minimum length of lower case should be `1`. +- `min_password_length` (Number) The minimum length required for the password. Enforces a stronger password policy by ensuring a minimum number of characters. Default minimum length is `6`. +- `min_special_characters` (Number) The minimum number of special characters (e.g., !, @, #, $, %) required in the password. This increases the password's security level by including symbols. Minimum special characters should be `1`. +- `min_uppercase_letters` (Number) The minimum number of uppercase letters (A-Z) required in the password. Helps ensure password complexity with a mix of case-sensitive characters. Minimum length of upper case should be `1`. +- `password_expiry_days` (Number) The number of days before the password expires. Must be between 1 and 1000 days. Defines how often passwords must be changed. Default is `999` days for expiry. +- `password_regex` (String) A regular expression (regex) to define custom password patterns, such as enforcing specific characters or sequences in the password. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `delete` (String) +- `update` (String) \ No newline at end of file diff --git a/examples/resources/spectrocloud_password_policy/providers.tf b/examples/resources/spectrocloud_password_policy/providers.tf new file mode 100644 index 00000000..4c109161 --- /dev/null +++ b/examples/resources/spectrocloud_password_policy/providers.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + spectrocloud = { + version = ">= 0.1" + source = "spectrocloud/spectrocloud" + } + } +} + +provider "spectrocloud" { + host = var.sc_host + api_key = var.sc_api_key + project_name = var.sc_project_name +} diff --git a/examples/resources/spectrocloud_password_policy/resource.tf b/examples/resources/spectrocloud_password_policy/resource.tf new file mode 100644 index 00000000..13aa6359 --- /dev/null +++ b/examples/resources/spectrocloud_password_policy/resource.tf @@ -0,0 +1,16 @@ +resource "spectrocloud_password_policy" "policy_regex" { + # password_regex = "*" + password_expiry_days = 999 + first_reminder_days = 5 + min_password_length = 6 + min_digits = 1 + min_lowercase_letters = 1 + min_special_characters = 1 + min_uppercase_letters = 1 +} + +## import existing password policy +#import { +# to = spectrocloud_password_policy.password_policy +# id = "password-policy" // tenant-uid +#} \ No newline at end of file diff --git a/examples/resources/spectrocloud_password_policy/terraform.template.tfvars b/examples/resources/spectrocloud_password_policy/terraform.template.tfvars new file mode 100644 index 00000000..c7e9d50b --- /dev/null +++ b/examples/resources/spectrocloud_password_policy/terraform.template.tfvars @@ -0,0 +1,4 @@ +# Spectro Cloud credentials +sc_host = "{Enter Spectro Cloud API Host}" #e.g: api.spectrocloud.com (for SaaS) +sc_api_key = "{Enter Spectro Cloud API Key}" +sc_project_name = "{Enter Spectro Cloud Project Name}" #e.g: Default \ No newline at end of file diff --git a/examples/resources/spectrocloud_password_policy/variables.tf b/examples/resources/spectrocloud_password_policy/variables.tf new file mode 100644 index 00000000..7bebf92c --- /dev/null +++ b/examples/resources/spectrocloud_password_policy/variables.tf @@ -0,0 +1,18 @@ +variable "sc_host" { + description = "Spectro Cloud Endpoint" + default = "api.spectrocloud.com" +} + +variable "sc_api_key" { + description = "Spectro Cloud API key" +} + +variable "sc_project_name" { + description = "Spectro Cloud Project (e.g: Default)" + default = "Default" +} + +variable "ssh_key_value" { + description = "ssh key value" + default = "ssh-rsa ...... == test@test.com" +} \ No newline at end of file diff --git a/go.mod b/go.mod index 4b891bbe..cfdc801c 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,8 @@ require ( github.com/robfig/cron v1.2.0 github.com/spectrocloud/gomi v1.14.1-0.20240214074114-c19394812368 github.com/spectrocloud/hapi v1.14.1-0.20240214071352-81f589b1d86d - github.com/spectrocloud/palette-sdk-go v0.0.0-20241119151816-43a4f46d482e - github.com/stretchr/testify v1.9.0 + github.com/spectrocloud/palette-sdk-go v0.0.0-20241219153631-ca32d3fd7126 + github.com/stretchr/testify v1.10.0 gotest.tools v2.2.0+incompatible k8s.io/api v0.23.5 k8s.io/apimachinery v0.23.5 @@ -72,7 +72,7 @@ require ( github.com/imdario/mergo v0.3.15 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mitchellh/cli v1.1.5 // indirect @@ -100,17 +100,18 @@ require ( github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.14.1 // indirect - go.mongodb.org/mongo-driver v1.16.0 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect - golang.org/x/crypto v0.22.0 // indirect + go.mongodb.org/mongo-driver v1.17.1 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.33.0 // indirect + go.opentelemetry.io/otel/metric v1.33.0 // indirect + go.opentelemetry.io/otel/trace v1.33.0 // indirect + golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect - golang.org/x/mod v0.13.0 // indirect + golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.21.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.17.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect google.golang.org/grpc v1.57.1 // indirect diff --git a/go.sum b/go.sum index e39c9b25..24ffef72 100644 --- a/go.sum +++ b/go.sum @@ -445,8 +445,9 @@ github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -568,8 +569,8 @@ github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfm github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -600,8 +601,8 @@ github.com/spectrocloud/gomi v1.14.1-0.20240214074114-c19394812368 h1:eY0BOyEbGu github.com/spectrocloud/gomi v1.14.1-0.20240214074114-c19394812368/go.mod h1:LlZ9We4kDaELYi7Is0SVmnySuDhwphJLS6ZT4wXxFIk= github.com/spectrocloud/hapi v1.14.1-0.20240214071352-81f589b1d86d h1:OMRbHxMJ1a+G1BYzvUYuMM0wLkYJPdnEOFx16faQ/UY= github.com/spectrocloud/hapi v1.14.1-0.20240214071352-81f589b1d86d/go.mod h1:MktpRPnSXDTHsQrFSD+daJFQ1zMLSR+1gWOL31jVvWE= -github.com/spectrocloud/palette-sdk-go v0.0.0-20241119151816-43a4f46d482e h1:l6PSTaV0PJMaalqKPhOiWP+I6UwAxgU/2kpXTfdPC/w= -github.com/spectrocloud/palette-sdk-go v0.0.0-20241119151816-43a4f46d482e/go.mod h1:dSlNvDS0qwUWTbrYI6P8x981mcbbRHFrBg67v5zl81U= +github.com/spectrocloud/palette-sdk-go v0.0.0-20241219153631-ca32d3fd7126 h1:jsUjl47xKbjaRFTU02PztidQiFNAj5i8S9A1nDMfRh8= +github.com/spectrocloud/palette-sdk-go v0.0.0-20241219153631-ca32d3fd7126/go.mod h1:Zv1+/Imw/lIOPAa+q9TzdyKiXmIzfLSwVTj11WemIZc= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= @@ -628,8 +629,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -662,8 +663,8 @@ go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lL go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= -go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4= -go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= +go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= +go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -671,16 +672,18 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= +go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= +go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= @@ -688,8 +691,8 @@ go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35 go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= +go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -715,8 +718,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -757,8 +760,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -843,8 +846,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -925,8 +928,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -945,8 +948,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1015,8 +1018,8 @@ golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpd golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/spectrocloud/cluster_common.go b/spectrocloud/cluster_common.go index 96747eeb..1fe5ef69 100644 --- a/spectrocloud/cluster_common.go +++ b/spectrocloud/cluster_common.go @@ -30,6 +30,11 @@ var ( //clusterVsphereKeys = []string{"name", "context", "tags", "description", "cluster_meta_attribute", "cluster_profile", "apply_setting", "cloud_account_id", "cloud_config_id", "review_repave_state", "pause_agent_upgrades", "os_patch_on_boot", "os_patch_schedule", "os_patch_after", "kubeconfig", "admin_kube_config", "cloud_config", "machine_pool", "backup_policy", "scan_policy", "cluster_rbac_binding", "namespaces", "host_config", "location_config", "skip_completion", "force_delete", "force_delete_delay"} ) +const ( + tenantString = "tenant" + projectString = "project" +) + func toNtpServers(in map[string]interface{}) []string { servers := make([]string, 0, 1) if _, ok := in["ntp_servers"]; ok { diff --git a/spectrocloud/provider.go b/spectrocloud/provider.go index edb24151..a15c6faf 100644 --- a/spectrocloud/provider.go +++ b/spectrocloud/provider.go @@ -137,11 +137,12 @@ func New(_ string) func() *schema.Provider { "spectrocloud_appliance": resourceAppliance(), - "spectrocloud_workspace": resourceWorkspace(), - "spectrocloud_alert": resourceAlert(), - "spectrocloud_ssh_key": resourceSSHKey(), - "spectrocloud_user": resourceUser(), - "spectrocloud_role": resourceRole(), + "spectrocloud_workspace": resourceWorkspace(), + "spectrocloud_alert": resourceAlert(), + "spectrocloud_ssh_key": resourceSSHKey(), + "spectrocloud_user": resourceUser(), + "spectrocloud_role": resourceRole(), + "spectrocloud_password_policy": resourcePasswordPolicy(), }, DataSourcesMap: map[string]*schema.Resource{ "spectrocloud_permission": dataSourcePermission(), diff --git a/spectrocloud/resource_password_policy.go b/spectrocloud/resource_password_policy.go new file mode 100644 index 00000000..93784f83 --- /dev/null +++ b/spectrocloud/resource_password_policy.go @@ -0,0 +1,252 @@ +package spectrocloud + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/spectrocloud/palette-sdk-go/api/models" + "time" +) + +func resourcePasswordPolicy() *schema.Resource { + return &schema.Resource{ + CreateContext: resourcePasswordPolicyCreate, + ReadContext: resourcePasswordPolicyRead, + UpdateContext: resourcePasswordPolicyUpdate, + DeleteContext: resourcePasswordPolicyDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourcePasswordPolicyImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + SchemaVersion: 2, + Schema: map[string]*schema.Schema{ + "password_regex": { + Type: schema.TypeString, + Optional: true, + Default: "", + ConflictsWith: []string{"min_password_length", "min_uppercase_letters", + "min_digits", "min_lowercase_letters", "min_special_characters"}, + RequiredWith: []string{"password_expiry_days", "first_reminder_days"}, + Description: "A regular expression (regex) to define custom password patterns, such as enforcing specific characters or sequences in the password.", + }, + "password_expiry_days": { + Type: schema.TypeInt, + Optional: true, + Default: 999, + ValidateFunc: validation.IntBetween(1, 1000), + Description: "The number of days before the password expires. Must be between 1 and 1000 days. Defines how often passwords must be changed. Default is `999` days for expiry.", + }, + "first_reminder_days": { + Type: schema.TypeInt, + Optional: true, + Default: 5, + Description: "The number of days before the password expiry to send the first reminder to the user. Default is `5` days before expiry.", + }, + "min_password_length": { + Type: schema.TypeInt, + Optional: true, + Description: "The minimum length required for the password. Enforces a stronger password policy by ensuring a minimum number of characters. Default minimum length is `6`.", + }, + "min_uppercase_letters": { + Type: schema.TypeInt, + Optional: true, + Description: "The minimum number of uppercase letters (A-Z) required in the password. Helps ensure password complexity with a mix of case-sensitive characters. Minimum length of upper case should be `1`.", + }, + "min_digits": { + Type: schema.TypeInt, + Optional: true, + Description: "The minimum number of numeric digits (0-9) required in the password. Ensures that passwords contain numerical characters. Minimum length of digit should be `1`.", + }, + "min_lowercase_letters": { + Type: schema.TypeInt, + Optional: true, + Description: "The minimum number of lowercase letters (a-z) required in the password. Ensures that lowercase characters are included for password complexity. Minimum length of lower case should be `1`.", + }, + "min_special_characters": { + Type: schema.TypeInt, + Optional: true, + Description: "The minimum number of special characters (e.g., !, @, #, $, %) required in the password. This increases the password's security level by including symbols. Minimum special characters should be `1`.", + }, + }, + } +} + +func toPasswordPolicy(d *schema.ResourceData) (*models.V1TenantPasswordPolicyEntity, error) { + if d.Get("password_regex").(string) != "" { + return &models.V1TenantPasswordPolicyEntity{ + IsRegex: true, + Regex: d.Get("password_regex").(string), + ExpiryDurationInDays: int64(d.Get("password_expiry_days").(int)), + FirstReminderInDays: int64(d.Get("first_reminder_days").(int)), + }, nil + } + return &models.V1TenantPasswordPolicyEntity{ + ExpiryDurationInDays: int64(d.Get("password_expiry_days").(int)), + FirstReminderInDays: int64(d.Get("first_reminder_days").(int)), + IsRegex: false, + MinLength: int64(d.Get("min_password_length").(int)), + MinNumOfBlockLetters: int64(d.Get("min_uppercase_letters").(int)), + MinNumOfDigits: int64(d.Get("min_digits").(int)), + MinNumOfSmallLetters: int64(d.Get("min_lowercase_letters").(int)), + MinNumOfSpecialCharacters: int64(d.Get("min_special_characters").(int)), + Regex: "", + }, nil +} + +func toPasswordPolicyDefault(d *schema.ResourceData) (*models.V1TenantPasswordPolicyEntity, error) { + return &models.V1TenantPasswordPolicyEntity{ + ExpiryDurationInDays: 999, + FirstReminderInDays: 5, + IsRegex: false, + MinLength: 6, + MinNumOfBlockLetters: 1, + MinNumOfDigits: 1, + MinNumOfSmallLetters: 1, + MinNumOfSpecialCharacters: 1, + Regex: "", + }, nil +} + +func flattenPasswordPolicy(passwordPolicy *models.V1TenantPasswordPolicyEntity, d *schema.ResourceData) error { + var err error + if passwordPolicy.Regex != "" { + err = d.Set("password_regex", passwordPolicy.Regex) + if err != nil { + return err + } + } else { + err = d.Set("min_password_length", passwordPolicy.MinLength) + if err != nil { + return err + } + err = d.Set("min_uppercase_letters", passwordPolicy.MinNumOfBlockLetters) + if err != nil { + return err + } + err = d.Set("min_digits", passwordPolicy.MinNumOfDigits) + if err != nil { + return err + } + err = d.Set("min_lowercase_letters", passwordPolicy.MinNumOfSmallLetters) + if err != nil { + return err + } + err = d.Set("min_special_characters", passwordPolicy.MinNumOfSpecialCharacters) + if err != nil { + return err + } + } + err = d.Set("password_expiry_days", passwordPolicy.ExpiryDurationInDays) + if err != nil { + return err + } + err = d.Set("first_reminder_days", passwordPolicy.FirstReminderInDays) + if err != nil { + return err + } + + return nil +} + +func resourcePasswordPolicyCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := getV1ClientWithResourceContext(m, tenantString) + var diags diag.Diagnostics + passwordPolicy, err := toPasswordPolicy(d) + if err != nil { + return diag.FromErr(err) + } + tenantUID, err := c.GetTenantUID() + if err != nil { + return diag.FromErr(err) + } + // For Password Policy we don't have support for creation it's always an update + err = c.UpdatePasswordPolicy(tenantUID, passwordPolicy) + if err != nil { + return diag.FromErr(err) + } + d.SetId("default-password-policy-id") + return diags +} + +func resourcePasswordPolicyRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := getV1ClientWithResourceContext(m, tenantString) + var diags diag.Diagnostics + tenantUID, err := c.GetTenantUID() + if err != nil { + return diag.FromErr(err) + } + resp, err := c.GetPasswordPolicy(tenantUID) + if err != nil { + return diag.FromErr(err) + } + err = flattenPasswordPolicy(resp, d) + if err != nil { + return nil + } + return diags +} + +func resourcePasswordPolicyUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := getV1ClientWithResourceContext(m, tenantString) + var diags diag.Diagnostics + passwordPolicy, err := toPasswordPolicy(d) + if err != nil { + return diag.FromErr(err) + } + tenantUID, err := c.GetTenantUID() + if err != nil { + return diag.FromErr(err) + } + // For Password Policy we don't have support for creation it's always an update + err = c.UpdatePasswordPolicy(tenantUID, passwordPolicy) + if err != nil { + return diag.FromErr(err) + } + return diags +} + +func resourcePasswordPolicyDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := getV1ClientWithResourceContext(m, tenantString) + var diags diag.Diagnostics + // We can't delete the base password policy, instead + passwordPolicy, err := toPasswordPolicyDefault(d) + if err != nil { + return diag.FromErr(err) + } + tenantUID, err := c.GetTenantUID() + if err != nil { + return diag.FromErr(err) + } + // For Password Policy we don't have support for creation it's always an update + err = c.UpdatePasswordPolicy(tenantUID, passwordPolicy) + if err != nil { + return diag.FromErr(err) + } + d.SetId("") + return diags +} + +func resourcePasswordPolicyImport(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + c := getV1ClientWithResourceContext(m, "tenant") + var diags diag.Diagnostics + givenTenantId := d.Id() + actualTenantId, err := c.GetTenantUID() + if err != nil { + return nil, err + } + if givenTenantId != actualTenantId { + return nil, fmt.Errorf("tenant id is not valid with curent user: %v", diags) + } + diags = resourcePasswordPolicyRead(ctx, d, m) + if diags.HasError() { + return nil, fmt.Errorf("could not read password policy for import: %v", diags) + } + return []*schema.ResourceData{d}, nil +} diff --git a/spectrocloud/resource_password_policy_test.go b/spectrocloud/resource_password_policy_test.go new file mode 100644 index 00000000..72c4ba70 --- /dev/null +++ b/spectrocloud/resource_password_policy_test.go @@ -0,0 +1,187 @@ +package spectrocloud + +import ( + "github.com/spectrocloud/palette-sdk-go/api/models" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/stretchr/testify/assert" +) + +func TestToPasswordPolicy(t *testing.T) { + resourceSchema := map[string]*schema.Schema{ + "password_regex": { + Type: schema.TypeString, + Optional: true, + }, + "password_expiry_days": { + Type: schema.TypeInt, + Optional: true, + }, + "first_reminder_days": { + Type: schema.TypeInt, + Optional: true, + }, + "min_password_length": { + Type: schema.TypeInt, + Optional: true, + }, + "min_uppercase_letters": { + Type: schema.TypeInt, + Optional: true, + }, + "min_digits": { + Type: schema.TypeInt, + Optional: true, + }, + "min_lowercase_letters": { + Type: schema.TypeInt, + Optional: true, + }, + "min_special_characters": { + Type: schema.TypeInt, + Optional: true, + }, + } + + testCases := []struct { + name string + input map[string]interface{} + expected *models.V1TenantPasswordPolicyEntity + expectError bool + }{ + { + name: "Password regex defined", + input: map[string]interface{}{ + "password_regex": "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d).+$", + "password_expiry_days": 90, + "first_reminder_days": 10, + }, + expected: &models.V1TenantPasswordPolicyEntity{ + IsRegex: true, + Regex: "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d).+$", + ExpiryDurationInDays: 90, + FirstReminderInDays: 10, + }, + expectError: false, + }, + { + name: "No regex, full policy specified", + input: map[string]interface{}{ + "password_regex": "", + "password_expiry_days": 90, + "first_reminder_days": 10, + "min_password_length": 12, + "min_uppercase_letters": 2, + "min_digits": 3, + "min_lowercase_letters": 4, + "min_special_characters": 1, + }, + expected: &models.V1TenantPasswordPolicyEntity{ + IsRegex: false, + Regex: "", + ExpiryDurationInDays: 90, + FirstReminderInDays: 10, + MinLength: 12, + MinNumOfBlockLetters: 2, + MinNumOfDigits: 3, + MinNumOfSmallLetters: 4, + MinNumOfSpecialCharacters: 1, + }, + expectError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resourceData := schema.TestResourceDataRaw(t, resourceSchema, tc.input) + result, err := toPasswordPolicy(resourceData) + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, result) + } + }) + } +} + +func TestToPasswordPolicyDefault(t *testing.T) { + resourceSchema := map[string]*schema.Schema{} + + resourceData := schema.TestResourceDataRaw(t, resourceSchema, map[string]interface{}{}) + result, err := toPasswordPolicyDefault(resourceData) + + assert.NoError(t, err) + expected := &models.V1TenantPasswordPolicyEntity{ + ExpiryDurationInDays: 999, + FirstReminderInDays: 5, + IsRegex: false, + MinLength: 6, + MinNumOfBlockLetters: 1, + MinNumOfDigits: 1, + MinNumOfSmallLetters: 1, + MinNumOfSpecialCharacters: 1, + Regex: "", + } + assert.Equal(t, expected, result) +} + +func TestFlattenPasswordPolicy(t *testing.T) { + resourceSchema := map[string]*schema.Schema{ + "password_regex": {Type: schema.TypeString, Optional: true}, + "password_expiry_days": {Type: schema.TypeInt, Optional: true}, + "first_reminder_days": {Type: schema.TypeInt, Optional: true}, + "min_password_length": {Type: schema.TypeInt, Optional: true}, + "min_uppercase_letters": {Type: schema.TypeInt, Optional: true}, + "min_digits": {Type: schema.TypeInt, Optional: true}, + "min_lowercase_letters": {Type: schema.TypeInt, Optional: true}, + "min_special_characters": {Type: schema.TypeInt, Optional: true}, + } + + resourceData := schema.TestResourceDataRaw(t, resourceSchema, map[string]interface{}{}) + + t.Run("with regex", func(t *testing.T) { + passwordPolicy := &models.V1TenantPasswordPolicyEntity{ + Regex: "^[a-zA-Z0-9]+$", + ExpiryDurationInDays: 90, + FirstReminderInDays: 10, + } + + err := flattenPasswordPolicy(passwordPolicy, resourceData) + assert.NoError(t, err) + + assert.Equal(t, "^[a-zA-Z0-9]+$", resourceData.Get("password_regex")) + assert.Equal(t, 90, resourceData.Get("password_expiry_days")) + assert.Equal(t, 10, resourceData.Get("first_reminder_days")) + }) + + t.Run("without regex", func(t *testing.T) { + passwordPolicy := &models.V1TenantPasswordPolicyEntity{ + ExpiryDurationInDays: 90, + FirstReminderInDays: 10, + MinLength: 8, + MinNumOfBlockLetters: 2, + MinNumOfDigits: 2, + MinNumOfSmallLetters: 2, + MinNumOfSpecialCharacters: 1, + Regex: "", + } + err := resourceData.Set("password_regex", "") + if err != nil { + return + } + err = flattenPasswordPolicy(passwordPolicy, resourceData) + assert.NoError(t, err) + + assert.Equal(t, "", resourceData.Get("password_regex")) + assert.Equal(t, 90, resourceData.Get("password_expiry_days")) + assert.Equal(t, 10, resourceData.Get("first_reminder_days")) + assert.Equal(t, 8, resourceData.Get("min_password_length")) + assert.Equal(t, 2, resourceData.Get("min_uppercase_letters")) + assert.Equal(t, 2, resourceData.Get("min_digits")) + assert.Equal(t, 2, resourceData.Get("min_lowercase_letters")) + assert.Equal(t, 1, resourceData.Get("min_special_characters")) + }) +} diff --git a/templates/resources/password_policy.md.tmpl b/templates/resources/password_policy.md.tmpl new file mode 100644 index 00000000..48a532ca --- /dev/null +++ b/templates/resources/password_policy.md.tmpl @@ -0,0 +1,40 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} + +You can learn more about managing password policy in Palette by reviewing the [Password Policy](https://docs.spectrocloud.com/enterprise-version/system-management/account-management/credentials/#password-requirements-and-security) guide. + +~> The password_policy resource enforces a password compliance policy. By default, a password policy is configured in Palette with default values. Users can update the password compliance settings as per their requirements. When a spectrocloud_password_policy resource is destroyed, the password policy will revert to the Palette default settings. + +## Example Usage + +An example of managing an password policy in Palette. + +```hcl +resource "spectrocloud_password_policy" "policy_regex" { + # password_regex = "*" + password_expiry_days = 123 + first_reminder_days = 5 + min_digits = 1 + min_lowercase_letters = 12 + min_password_length = 12 + min_special_characters = 1 + min_uppercase_letters = 1 +} + +## import existing password policy +#import { +# to = spectrocloud_password_policy.password_policy +# id = "{tenantUID}" // tenant-uid. +#} + +``` + +{{ .SchemaMarkdown | trimspace }} \ No newline at end of file