-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Normalize CA strings before comparing them in plans (#106)
* Normalize CA strings before comparing them in plans Terraform, in general, does not approve of situations in which an API returns a value that is not exactly the same as the one that was given to it. In the case of the Temporal Cloud API, the accepted client CA may be returned in a "normalized" form, with whitespace stripped out. This commit implements a semantic equality check for encoded CAs by normalizing base64-encoded CA strings prior to comparing them. Fixes #102, #90, #87. * Skip equality check for empty CA strings
- Loading branch information
1 parent
8e7f816
commit de9343a
Showing
4 changed files
with
250 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
package types | ||
|
||
import ( | ||
"context" | ||
"crypto/x509" | ||
"encoding/base64" | ||
"encoding/pem" | ||
"errors" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework/attr" | ||
"github.com/hashicorp/terraform-plugin-framework/diag" | ||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes" | ||
"github.com/hashicorp/terraform-plugin-go/tftypes" | ||
) | ||
|
||
var ( | ||
_ basetypes.StringTypable = EncodedCAType{} | ||
_ basetypes.StringValuable = EncodedCAValue{} | ||
_ basetypes.StringValuableWithSemanticEquals = EncodedCAValue{} | ||
) | ||
|
||
type EncodedCAType struct { | ||
basetypes.StringType | ||
} | ||
|
||
func (t EncodedCAType) Equal(o attr.Type) bool { | ||
other, ok := o.(EncodedCAType) | ||
if !ok { | ||
return false | ||
} | ||
|
||
return t.StringType.Equal(other.StringType) | ||
} | ||
|
||
func (t EncodedCAType) ValueFromString(ctx context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) { | ||
return EncodedCA(in.ValueString()), nil | ||
} | ||
|
||
func (t EncodedCAType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { | ||
attrValue, err := t.StringType.ValueFromTerraform(ctx, in) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
stringValue, ok := attrValue.(basetypes.StringValue) | ||
if !ok { | ||
return nil, errors.New("unexpected value type") | ||
} | ||
|
||
stringValuable, diags := t.ValueFromString(ctx, stringValue) | ||
if diags.HasError() { | ||
return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags) | ||
} | ||
|
||
return stringValuable, nil | ||
} | ||
|
||
type EncodedCAValue struct { | ||
basetypes.StringValue | ||
} | ||
|
||
func (v EncodedCAValue) Equal(o attr.Value) bool { | ||
other, ok := o.(EncodedCAValue) | ||
if !ok { | ||
return false | ||
} | ||
|
||
return v.StringValue.Equal(other.StringValue) | ||
} | ||
|
||
func (v EncodedCAValue) Type(ctx context.Context) attr.Type { | ||
return EncodedCAType{} | ||
} | ||
|
||
func EncodedCA(val string) EncodedCAValue { | ||
return EncodedCAValue{ | ||
StringValue: basetypes.NewStringValue(val), | ||
} | ||
} | ||
|
||
// normalizeCAString accepts a base64-encoded PEM string and normalizes it by removing extraneous whitespace. The | ||
// Temporal API will do this and cause a mismatch in the state if we don't also do it here. | ||
func normalizeCAString(certPEMBase64 string) (string, error) { | ||
certs, err := parseEncodedCertificates(certPEMBase64) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
var result []byte | ||
for _, c := range certs { | ||
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw}) | ||
result = append(result, pemBytes...) | ||
} | ||
|
||
return base64.StdEncoding.EncodeToString(result), nil | ||
} | ||
|
||
func parseEncodedCertificates(certPEMBase64 string) ([]*x509.Certificate, error) { | ||
certPEMBytes, err := base64.StdEncoding.DecodeString(certPEMBase64) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to decode base64-encoded PEM: %w", err) | ||
} | ||
|
||
if len(certPEMBytes) == 0 { | ||
return nil, fmt.Errorf("no certificates received") | ||
} | ||
|
||
var blocks []byte | ||
cursor := certPEMBytes | ||
for { | ||
var block *pem.Block | ||
block, cursor = pem.Decode(cursor) | ||
if block == nil { | ||
break | ||
} | ||
|
||
blocks = append(blocks, block.Bytes...) | ||
} | ||
|
||
// If p is greater than 0, then this means that there was a portion of the certificate that | ||
// is/was malformed. | ||
if len(cursor) > 0 && strings.TrimSpace(string(cursor)) != "" { | ||
return []*x509.Certificate{}, errors.New("malformed certificates") | ||
} | ||
return x509.ParseCertificates(blocks) | ||
} | ||
|
||
func (v EncodedCAValue) StringSemanticEquals(ctx context.Context, newValuable basetypes.StringValuable) (bool, diag.Diagnostics) { | ||
var diags diag.Diagnostics | ||
|
||
newValue, ok := newValuable.(EncodedCAValue) | ||
if !ok { | ||
diags.AddError( | ||
"Semantic Equality Check Error", | ||
"An unexpected value type was received while performing semantic equality checks. "+ | ||
"Please report this to the provider developers.\n\n"+ | ||
"Expected Value Type: "+fmt.Sprintf("%T", v)+"\n"+ | ||
"Got Value Type: "+fmt.Sprintf("%T", newValuable), | ||
) | ||
|
||
return false, diags | ||
} | ||
|
||
// Normalize the certificate strings before comparing them. | ||
normalizedV, err := normalizeCAString(v.ValueString()) | ||
if err != nil { | ||
diags.AddError("Certificate Normalization Error", "Failed to normalize the existing certificate: "+err.Error()) | ||
return false, diags | ||
} | ||
|
||
normalizedNewValue, err := normalizeCAString(newValue.ValueString()) | ||
if err != nil { | ||
// The new value may not be a valid CA string. This will get rejected elsewhere in the plan. Since this is just | ||
// an equality check, we should return false and continue. | ||
return false, diags | ||
} | ||
|
||
return normalizedV == normalizedNewValue, diags | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package types | ||
|
||
import "testing" | ||
|
||
func TestCACertNormalization(t *testing.T) { | ||
input := "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJ5VENDQVZDZ0F3SUJBZ0lSQVdIa0MrNkpVZjNzOVRxNDNtZHAyemd3Q2dZSUtvWkl6ajBFQXdNd0V6RVIKTUE4R0ExVUVDaE1JZEdWdGNHOXlZV3d3SGhjTk1qTXdPREV3TURBd09UUTFXaGNOTWpRd09EQTVNREF4TURRMQpXakFUTVJFd0R3WURWUVFLRXdoMFpXMXdiM0poYkRCMk1CQUdCeXFHU000OUFnRUdCU3VCQkFBaUEySUFCQ3pRCjdEd3dHU1FLTTZacngzUXR3N0l1YmZ4aUozUlNYQ3FtY0doRWJGVmVvY3dBZEVnTVlsd1NsVWlXdERaVlIyZE0KWE05VVpMV0s0YUdHbkROUzVNaGN6NmliU0JTN093ZjR0UlpaQTlTcEZDak53MkhyYWFpVVZWK0VVZ3hvZTZObwpNR1l3RGdZRFZSMFBBUUgvQkFRREFnR0dNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdIUVlEVlIwT0JCWUVGRzROCjhsSVhxUUt4d1ZzL2l4VnpkRjZYR1ptK01DUUdBMVVkRVFRZE1CdUNHV05zYVdWdWRDNXliMjkwTG5SbGJYQnYKY21Gc0xsQjFWSE13Q2dZSUtvWkl6ajBFQXdNRFp3QXdaQUl3UkxmbTlTN3JLR2QzMEtkUXZVTWNPY0RKbG1Edwo2L29NNlVPSkZ4TGVHY3BZYmd4US9iRml6ZStZeDlROWtOZU1BakE3R2lGc2FpcGFLdFdIeTVNQ09DYXMzWlA2Cit0dExhWE5Yc3MzWjVXazV2aERRbnlFOEpSM3JQZVEyY0hYTGlBMD0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQoKCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlCeGpDQ0FVMmdBd0lCQWdJUkE3b2E2dnhqd3RvVHdFdjNhVnZoZWh3d0NnWUlLb1pJemowRUF3TXdFakVRCk1BNEdBMVVFQ2hNSGRHVnpkR2x1WnpBZUZ3MHlOREE0TURJeE5qUXhOVFJhRncweU5UQTRNREl4TmpReU5UUmEKTUJJeEVEQU9CZ05WQkFvVEIzUmxjM1JwYm1jd2RqQVFCZ2NxaGtqT1BRSUJCZ1VyZ1FRQUlnTmlBQVFDcVVqVApEUVVKN2t3a055K3hkc0l3TitEY2hvSmJjdWVQVk9FQTB5STR0M05jS3lDcDJSTjhkbVAzbjFidVhtVVFNODBFCmxsQVlNaDFHcEU3VW1oT1l2aVVXenFWajNmN0s1Qm8wT1QvUjFxcndxVldXL0ZvbU5vdVlxK3o4TUxTalp6QmwKTUE0R0ExVWREd0VCL3dRRUF3SUJoakFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVdCQlJPVS9FSAp4Q1dDWmNJemVTcnR0NGhIV1ozY3VUQWpCZ05WSFJFRUhEQWFnaGhqYkdsbGJuUXVjbTl2ZEM1MFpYTjBhVzVuCkxsRnNSRk13Q2dZSUtvWkl6ajBFQXdNRFp3QXdaQUl3VlJJSEFzbmExajdUZnllQWR4YUNpY2dNK1lHcTQwVTQKUTdjOThCVlg3M1h1NkFnWXlBVU41eGlvbkJZSklCU3FBakFMcWRkanFzVG9pN0hXMTd5STVuN1VzSUdabHdrcQoxMm9rQjFJR0JSbU9pOU1SSnhuVk0wZXhrWGFUaEhGZ0toYz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==" | ||
expected := "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJ5VENDQVZDZ0F3SUJBZ0lSQVdIa0MrNkpVZjNzOVRxNDNtZHAyemd3Q2dZSUtvWkl6ajBFQXdNd0V6RVIKTUE4R0ExVUVDaE1JZEdWdGNHOXlZV3d3SGhjTk1qTXdPREV3TURBd09UUTFXaGNOTWpRd09EQTVNREF4TURRMQpXakFUTVJFd0R3WURWUVFLRXdoMFpXMXdiM0poYkRCMk1CQUdCeXFHU000OUFnRUdCU3VCQkFBaUEySUFCQ3pRCjdEd3dHU1FLTTZacngzUXR3N0l1YmZ4aUozUlNYQ3FtY0doRWJGVmVvY3dBZEVnTVlsd1NsVWlXdERaVlIyZE0KWE05VVpMV0s0YUdHbkROUzVNaGN6NmliU0JTN093ZjR0UlpaQTlTcEZDak53MkhyYWFpVVZWK0VVZ3hvZTZObwpNR1l3RGdZRFZSMFBBUUgvQkFRREFnR0dNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdIUVlEVlIwT0JCWUVGRzROCjhsSVhxUUt4d1ZzL2l4VnpkRjZYR1ptK01DUUdBMVVkRVFRZE1CdUNHV05zYVdWdWRDNXliMjkwTG5SbGJYQnYKY21Gc0xsQjFWSE13Q2dZSUtvWkl6ajBFQXdNRFp3QXdaQUl3UkxmbTlTN3JLR2QzMEtkUXZVTWNPY0RKbG1Edwo2L29NNlVPSkZ4TGVHY3BZYmd4US9iRml6ZStZeDlROWtOZU1BakE3R2lGc2FpcGFLdFdIeTVNQ09DYXMzWlA2Cit0dExhWE5Yc3MzWjVXazV2aERRbnlFOEpSM3JQZVEyY0hYTGlBMD0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQnhqQ0NBVTJnQXdJQkFnSVJBN29hNnZ4and0b1R3RXYzYVZ2aGVod3dDZ1lJS29aSXpqMEVBd013RWpFUQpNQTRHQTFVRUNoTUhkR1Z6ZEdsdVp6QWVGdzB5TkRBNE1ESXhOalF4TlRSYUZ3MHlOVEE0TURJeE5qUXlOVFJhCk1CSXhFREFPQmdOVkJBb1RCM1JsYzNScGJtY3dkakFRQmdjcWhrak9QUUlCQmdVcmdRUUFJZ05pQUFRQ3FValQKRFFVSjdrd2tOeSt4ZHNJd04rRGNob0piY3VlUFZPRUEweUk0dDNOY0t5Q3AyUk44ZG1QM24xYnVYbVVRTTgwRQpsbEFZTWgxR3BFN1VtaE9ZdmlVV3pxVmozZjdLNUJvME9UL1IxcXJ3cVZXVy9Gb21Ob3VZcSt6OE1MU2paekJsCk1BNEdBMVVkRHdFQi93UUVBd0lCaGpBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJST1UvRUgKeENXQ1pjSXplU3J0dDRoSFdaM2N1VEFqQmdOVkhSRUVIREFhZ2hoamJHbGxiblF1Y205dmRDNTBaWE4wYVc1bgpMbEZzUkZNd0NnWUlLb1pJemowRUF3TURad0F3WkFJd1ZSSUhBc25hMWo3VGZ5ZUFkeGFDaWNnTStZR3E0MFU0ClE3Yzk4QlZYNzNYdTZBZ1l5QVVONXhpb25CWUpJQlNxQWpBTHFkZGpxc1RvaTdIVzE3eUk1bjdVc0lHWmx3a3EKMTJva0IxSUdCUm1PaTlNUkp4blZNMGV4a1hhVGhIRmdLaGM9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" | ||
normalized, err := normalizeCAString(input) | ||
if err != nil { | ||
t.Fatalf("failed to normalize cert: %v", err) | ||
} | ||
|
||
if normalized != expected { | ||
t.Fatalf("unexpected normalized cert: %s", normalized) | ||
} | ||
} |