Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent panic on random_string and random_password when character set is empty #551

Merged
merged 4 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/unreleased/BUG FIXES-20240410-175423.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: BUG FIXES
body: 'provider/random_password: Fix bug which causes panic when special, upper, lower
and number/numeric are all false'
time: 2024-04-10T17:54:23.219223+01:00
custom:
Issue: "551"
6 changes: 6 additions & 0 deletions .changes/unreleased/BUG FIXES-20240410-175536.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: BUG FIXES
body: 'provider/random_string: Fix bug which causes panic when special, upper, lower
and number/numeric are all false'
time: 2024-04-10T17:55:36.979987+01:00
custom:
Issue: "551"
4 changes: 2 additions & 2 deletions docs/resources/password.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ resource "aws_db_instance" "example" {
- `min_numeric` (Number) Minimum number of numeric characters in the result. Default value is `0`.
- `min_special` (Number) Minimum number of special characters in the result. Default value is `0`.
- `min_upper` (Number) Minimum number of uppercase alphabet characters in the result. Default value is `0`.
- `number` (Boolean, Deprecated) Include numeric characters in the result. Default value is `true`. **NOTE**: This is deprecated, use `numeric` instead.
- `numeric` (Boolean) Include numeric characters in the result. Default value is `true`.
- `number` (Boolean, Deprecated) Include numeric characters in the result. Default value is `true`. If `number`, `upper`, `lower`, and `special` are all configured, at least one of them must be set to `true`. **NOTE**: This is deprecated, use `numeric` instead.
- `numeric` (Boolean) Include numeric characters in the result. Default value is `true`. If `numeric`, `upper`, `lower`, and `special` are all configured, at least one of them must be set to `true`.
- `override_special` (String) Supply your own list of special characters to use for string generation. This overrides the default character list in the special argument. The `special` argument must still be set to true for any overwritten characters to be used in generation.
- `special` (Boolean) Include special characters in the result. These are `!@#$%&*()-_=+[]{}<>:?`. Default value is `true`.
- `upper` (Boolean) Include uppercase alphabet characters in the result. Default value is `true`.
Expand Down
4 changes: 2 additions & 2 deletions docs/resources/string.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ resource "random_string" "random" {
- `min_numeric` (Number) Minimum number of numeric characters in the result. Default value is `0`.
- `min_special` (Number) Minimum number of special characters in the result. Default value is `0`.
- `min_upper` (Number) Minimum number of uppercase alphabet characters in the result. Default value is `0`.
- `number` (Boolean, Deprecated) Include numeric characters in the result. Default value is `true`. **NOTE**: This is deprecated, use `numeric` instead.
- `numeric` (Boolean) Include numeric characters in the result. Default value is `true`.
- `number` (Boolean, Deprecated) Include numeric characters in the result. Default value is `true`. If `number`, `upper`, `lower`, and `special` are all configured, at least one of them must be set to `true`. **NOTE**: This is deprecated, use `numeric` instead.
- `numeric` (Boolean) Include numeric characters in the result. Default value is `true`. If `numeric`, `upper`, `lower`, and `special` are all configured, at least one of them must be set to `true`.
- `override_special` (String) Supply your own list of special characters to use for string generation. This overrides the default character list in the special argument. The `special` argument must still be set to true for any overwritten characters to be used in generation.
- `special` (Boolean) Include special characters in the result. These are `!@#$%&*()-_=+[]{}<>:?`. Default value is `true`.
- `upper` (Boolean) Include uppercase alphabet characters in the result. Default value is `true`.
Expand Down
25 changes: 22 additions & 3 deletions internal/provider/resource_password.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
mapplanmodifiers "github.com/terraform-providers/terraform-provider-random/internal/planmodifiers/map"
stringplanmodifiers "github.com/terraform-providers/terraform-provider-random/internal/planmodifiers/string"
"github.com/terraform-providers/terraform-provider-random/internal/random"
"github.com/terraform-providers/terraform-provider-random/internal/validators"
)

var (
Expand Down Expand Up @@ -622,6 +623,8 @@ func passwordSchemaV3() schema.Schema {

"number": schema.BoolAttribute{
Description: "Include numeric characters in the result. Default value is `true`. " +
"If `number`, `upper`, `lower`, and `special` are all configured, at least one " +
"of them must be set to `true`. " +
"**NOTE**: This is deprecated, use `numeric` instead.",
Optional: true,
Computed: true,
Expand All @@ -630,16 +633,32 @@ func passwordSchemaV3() schema.Schema {
boolplanmodifier.RequiresReplace(),
},
DeprecationMessage: "**NOTE**: This is deprecated, use `numeric` instead.",
Validators: []validator.Bool{
validators.AtLeastOneOfTrue(
path.MatchRoot("special"),
path.MatchRoot("upper"),
path.MatchRoot("lower"),
),
},
},

"numeric": schema.BoolAttribute{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we update the description of this attribute to indicate that it needs to be set with one of special, upper, or lower being true? Same with the number attribute.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good shout. I've updated the description for number and numeric for both resource_password and resource_string and regenerated the docs.

Description: "Include numeric characters in the result. Default value is `true`.",
Optional: true,
Computed: true,
Description: "Include numeric characters in the result. Default value is `true`. " +
"If `numeric`, `upper`, `lower`, and `special` are all configured, at least one " +
"of them must be set to `true`.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifiers.NumberNumericAttributePlanModifier(),
boolplanmodifier.RequiresReplace(),
},
Validators: []validator.Bool{
validators.AtLeastOneOfTrue(
path.MatchRoot("special"),
path.MatchRoot("upper"),
path.MatchRoot("lower"),
),
},
},

"min_numeric": schema.Int64Attribute{
Expand Down
96 changes: 96 additions & 0 deletions internal/provider/resource_password_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,47 @@ func TestGenerateHash(t *testing.T) {
}
}

func TestCreateString(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
input random.StringParams
expectedError error
}{
"input-false": {
input: random.StringParams{
Length: 16, // Required
Lower: false,
Numeric: false,
Special: false,
Upper: false,
},
expectedError: errors.New("the character set specified is empty"),
},
}

equateErrorMessage := cmp.Comparer(func(x, y error) bool {
if x == nil || y == nil {
return x == nil && y == nil
}
return x.Error() == y.Error()
})

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

_, err := random.CreateString(testCase.input)

if diff := cmp.Diff(testCase.expectedError, err, equateErrorMessage); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}

func TestAccResourcePassword_Import(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Expand Down Expand Up @@ -2718,6 +2759,61 @@ func TestAccResourcePassword_Keepers_FrameworkMigration_NullMapValueToValue(t *t
})
}

func TestAccResourcePassword_NumericFalse(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
Steps: []resource.TestStep{
{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Config: `resource "random_password" "test" {
length = 12
special = false
upper = false
lower = false
numeric = false
}`,
ExpectError: regexp.MustCompile(`At least one attribute out of \[special,upper,lower,numeric\] must be specified`),
},
},
})
}

func TestAccResourcePassword_NumberFalse(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
Steps: []resource.TestStep{
{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Config: `resource "random_password" "test" {
length = 12
special = false
upper = false
lower = false
number = false
}`,
ExpectError: regexp.MustCompile(`At least one attribute out of \[special,upper,lower,number\] must be specified`),
},
},
})
}

func TestAccResourcePassword_NumericNumberFalse(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
Steps: []resource.TestStep{
{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Config: `resource "random_password" "test" {
length = 12
special = false
upper = false
lower = false
numeric = false
number = false
}`,
ExpectError: regexp.MustCompile(`At least one attribute out of \[special,upper,lower,numeric\] must be specified((.|\n)*)At least one attribute out of \[special,upper,lower,number\] must be specified`),
},
},
})
}

func testBcryptHashInvalid(hash *string, password *string) resource.TestCheckFunc {
return func(_ *terraform.State) error {
if hash == nil || *hash == "" {
Expand Down
25 changes: 22 additions & 3 deletions internal/provider/resource_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
mapplanmodifiers "github.com/terraform-providers/terraform-provider-random/internal/planmodifiers/map"
stringplanmodifiers "github.com/terraform-providers/terraform-provider-random/internal/planmodifiers/string"
"github.com/terraform-providers/terraform-provider-random/internal/random"
"github.com/terraform-providers/terraform-provider-random/internal/validators"
)

var (
Expand Down Expand Up @@ -424,6 +425,8 @@ func stringSchemaV3() schema.Schema {

"number": schema.BoolAttribute{
Description: "Include numeric characters in the result. Default value is `true`. " +
"If `number`, `upper`, `lower`, and `special` are all configured, at least one " +
"of them must be set to `true`. " +
"**NOTE**: This is deprecated, use `numeric` instead.",
Optional: true,
Computed: true,
Expand All @@ -432,16 +435,32 @@ func stringSchemaV3() schema.Schema {
boolplanmodifier.RequiresReplace(),
},
DeprecationMessage: "**NOTE**: This is deprecated, use `numeric` instead.",
Validators: []validator.Bool{
validators.AtLeastOneOfTrue(
path.MatchRoot("special"),
path.MatchRoot("upper"),
path.MatchRoot("lower"),
),
},
},

"numeric": schema.BoolAttribute{
Description: "Include numeric characters in the result. Default value is `true`.",
Optional: true,
Computed: true,
Description: "Include numeric characters in the result. Default value is `true`. " +
"If `numeric`, `upper`, `lower`, and `special` are all configured, at least one " +
"of them must be set to `true`.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifiers.NumberNumericAttributePlanModifier(),
boolplanmodifier.RequiresReplace(),
},
Validators: []validator.Bool{
validators.AtLeastOneOfTrue(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nota Bene: Definitely don't need to change this, but another consideration here might be to introduce this multiple attribute validation at the resource level as a ConfigValidator and specify all 4 attributes in there. Maybe its just me but I tend to get a little confused reading attribute-based validators (remembering "oh yeah, it includes this attribute too") and whether that attribute-based validation should be added to all the relevant attributes (confusingly, no, since it will generate multiple similar diagnostics). 😅

Copy link
Contributor Author

@bendbennett bendbennett Apr 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to refactor along these lines, but if you're ok with the current implementation could you approve the PR if there's nothing blocking?

path.MatchRoot("special"),
path.MatchRoot("upper"),
path.MatchRoot("lower"),
),
},
},

"min_numeric": schema.Int64Attribute{
Expand Down
55 changes: 55 additions & 0 deletions internal/provider/resource_string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1758,6 +1758,61 @@ func TestAccResourceString_Import_FromVersion3_4_2(t *testing.T) {
})
}

func TestAccResourceString_NumericFalse(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
Steps: []resource.TestStep{
{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Config: `resource "random_string" "test" {
length = 12
special = false
upper = false
lower = false
numeric = false
}`,
ExpectError: regexp.MustCompile(`At least one attribute out of \[special,upper,lower,numeric\] must be specified`),
},
},
})
}

func TestAccResourceString_NumberFalse(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
Steps: []resource.TestStep{
{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Config: `resource "random_string" "test" {
length = 12
special = false
upper = false
lower = false
number = false
}`,
ExpectError: regexp.MustCompile(`At least one attribute out of \[special,upper,lower,number\] must be specified`),
},
},
})
}

func TestAccResourceString_NumericNumberFalse(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
Steps: []resource.TestStep{
{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Config: `resource "random_string" "test" {
length = 12
special = false
upper = false
lower = false
numeric = false
number = false
}`,
ExpectError: regexp.MustCompile(`At least one attribute out of \[special,upper,lower,numeric\] must be specified((.|\n)*)At least one attribute out of \[special,upper,lower,number\] must be specified`),
},
},
})
}

func testCheckLen(expectedLen int) func(input string) error {
return func(input string) error {
if len(input) != expectedLen {
Expand Down
13 changes: 13 additions & 0 deletions internal/random/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package random

import (
"crypto/rand"
"errors"
"math/big"
"sort"
)
Expand Down Expand Up @@ -47,6 +48,10 @@ func CreateString(input StringParams) ([]byte, error) {
chars += specialChars
}

if chars == "" {
return nil, errors.New("the character set specified is empty")
}

minMapping := map[string]int64{
numChars: input.MinNumeric,
lowerChars: input.MinLower,
Expand Down Expand Up @@ -84,6 +89,14 @@ func CreateString(input StringParams) ([]byte, error) {
}

func generateRandomBytes(charSet *string, length int64) ([]byte, error) {
if charSet == nil {
return nil, errors.New("charSet is nil")
}

if *charSet == "" && length > 0 {
return nil, errors.New("charSet is empty")
}

bytes := make([]byte, length)
setLen := big.NewInt(int64(len(*charSet)))
for i := range bytes {
Expand Down
Loading
Loading