Repository navigation
fix(aws-kms): InvalidMarkerException on list operations, key ARN in DisabledException #1682
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package kms_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "testing" | ||
|
|
||
| cerrors "github.com/stackshy/cloudemu/v2/errors" | ||
| "github.com/stackshy/cloudemu/v2/services/kms/driver" | ||
| ) | ||
|
|
||
| // TestDisabledKeyMessageNamesKey checks that every operation refused for a | ||
| // Disabled key returns DisabledException with the key ARN in the message, as | ||
| // real KMS does ("<key ARN> is disabled."), while errors.Is still matches | ||
| // driver.ErrKeyDisabled for callers such as Secrets Manager. | ||
| func TestDisabledKeyMessageNamesKey(t *testing.T) { | ||
| ctx := context.Background() | ||
| m := newMock(t) | ||
|
|
||
| k, err := m.CreateKey(ctx, driver.CreateKeyInput{}) | ||
| if err != nil { | ||
| t.Fatalf("CreateKey: %v", err) | ||
| } | ||
|
|
||
| if err := m.DisableKey(ctx, k.KeyID); err != nil { | ||
| t.Fatalf("DisableKey: %v", err) | ||
| } | ||
|
|
||
| want := k.ARN + " is disabled." | ||
|
|
||
| ops := map[string]func() error{ | ||
| "Encrypt": func() error { | ||
| _, err := m.Encrypt(ctx, driver.EncryptInput{KeyID: k.KeyID, Plaintext: []byte("x")}) | ||
| return err | ||
| }, | ||
| "GenerateDataKey": func() error { | ||
| _, err := m.GenerateDataKey(ctx, driver.GenerateDataKeyInput{KeyID: k.KeyID, KeySpec: "AES_256"}) | ||
| return err | ||
| }, | ||
| "EnableKeyRotation": func() error { | ||
| return m.EnableKeyRotation(ctx, k.KeyID, 0) | ||
| }, | ||
| } | ||
|
|
||
| for name, call := range ops { | ||
| t.Run(name, func(t *testing.T) { | ||
| err := call() | ||
| if !errors.Is(err, driver.ErrKeyDisabled) { | ||
| t.Fatalf("err = %v, want ErrKeyDisabled", err) | ||
| } | ||
|
|
||
| if got := cerrors.Message(err); got != want { | ||
| t.Fatalf("message = %q, want %q", got, want) | ||
| } | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package kms_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "testing" | ||
|
|
||
| "github.com/aws/aws-sdk-go-v2/aws" | ||
| awskms "github.com/aws/aws-sdk-go-v2/service/kms" | ||
| kmstypes "github.com/aws/aws-sdk-go-v2/service/kms/types" | ||
| ) | ||
|
|
||
| // TestSDKDisabledExceptionMessage checks that DisabledException reaches the | ||
| // client with the message real KMS sends, "<key ARN> is disabled.". | ||
| func TestSDKDisabledExceptionMessage(t *testing.T) { | ||
| ctx := context.Background() | ||
| c := newKMSClient(t) | ||
|
|
||
| key, err := c.CreateKey(ctx, &awskms.CreateKeyInput{}) | ||
| if err != nil { | ||
| t.Fatalf("CreateKey: %v", err) | ||
| } | ||
|
|
||
| if _, err := c.DisableKey(ctx, &awskms.DisableKeyInput{KeyId: key.KeyMetadata.KeyId}); err != nil { | ||
| t.Fatalf("DisableKey: %v", err) | ||
| } | ||
|
|
||
| _, err = c.Encrypt(ctx, &awskms.EncryptInput{KeyId: key.KeyMetadata.KeyId, Plaintext: []byte("x")}) | ||
|
|
||
| var disabled *kmstypes.DisabledException | ||
| if !errors.As(err, &disabled) { | ||
| t.Fatalf("want DisabledException, got %v", err) | ||
| } | ||
|
|
||
| want := aws.ToString(key.KeyMetadata.Arn) + " is disabled." | ||
| if got := disabled.ErrorMessage(); got != want { | ||
| t.Fatalf("message = %q, want %q", got, want) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,42 @@ | ||
| package kms | ||
|
|
||
| import ( | ||
| "unicode/utf8" | ||
|
|
||
| cerrors "github.com/stackshy/cloudemu/v2/errors" | ||
| "github.com/stackshy/cloudemu/v2/server/wire" | ||
| kmsdriver "github.com/stackshy/cloudemu/v2/services/kms/driver" | ||
| ) | ||
|
|
||
| // defaultPageLimit is the page size KMS list operations use when Limit is unset. | ||
| const defaultPageLimit = 100 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This default is right for ListKeys, ListKeyPolicies, ListKeyRotations and DescribeCustomKeyStores. ListAliases, ListGrants, ListRetirableGrants and ListResourceTags default to 50 in the API reference. With 60 aliases, |
||
|
|
||
| // maxMarkerLen is the modeled MarkerType length limit (1-1024). | ||
| const maxMarkerLen = 1024 | ||
|
|
||
| // markerFault says which exception a well-formed Marker that this server did | ||
| // not issue produces. Most KMS list operations model InvalidMarkerException; | ||
| // ListKeyPolicies does not, so it answers ValidationException. | ||
| type markerFault int | ||
|
|
||
| const ( | ||
| // markerInvalid answers InvalidMarkerException. | ||
| markerInvalid markerFault = iota | ||
| // markerValidation answers ValidationException, for operations whose error | ||
| // list has no InvalidMarkerException. | ||
| markerValidation | ||
| ) | ||
|
|
||
| // pageWindow resolves an opaque Marker + Limit into a [start,end) slice window | ||
| // over a stable result set of length total. It returns the NextMarker to hand | ||
| // back ("" on the last page) and whether the result was truncated. KMS keys its | ||
| // pagination on Marker/Limit and echoes Truncated/NextMarker; an offset-based | ||
| // marker is stable because each list op returns a deterministically ordered set. | ||
| func pageWindow(marker string, limit int32, total int) (start, end int, nextMarker string, truncated bool, err error) { | ||
| start, err = decodeMarker(marker) | ||
| // fault picks the exception for a Marker that decodes to no position. | ||
| func pageWindow( | ||
| marker string, limit int32, total int, fault markerFault, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Limit isn't range-checked. |
||
| ) (start, end int, nextMarker string, truncated bool, err error) { | ||
| start, err = decodeMarker(marker, fault) | ||
| if err != nil { | ||
| return 0, 0, "", false, err | ||
| } | ||
|
|
@@ -36,11 +58,45 @@ func pageWindow(marker string, limit int32, total int) (start, end int, nextMark | |
| return start, end, wire.EncodeOffset(end), true, nil | ||
| } | ||
|
|
||
| func decodeMarker(marker string) (int, error) { | ||
| // decodeMarker turns a Marker back into an offset. A Marker that breaks the | ||
| // modeled MarkerType constraints (length up to 1024, characters U+0020-U+00FF) | ||
| // fails request validation with ValidationException on every operation. A | ||
| // Marker that passes them but was not issued by this server is | ||
| // InvalidMarkerException where the operation models it. | ||
| func decodeMarker(marker string, fault markerFault) (int, error) { | ||
| if err := validateMarkerConstraint(marker); err != nil { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A Marker in the right format that this server never issued still passes. |
||
| return 0, err | ||
| } | ||
|
|
||
| n, err := wire.DecodeOffset(marker) | ||
| if err != nil { | ||
| if err == nil { | ||
| return n, nil | ||
| } | ||
|
|
||
| if fault == markerValidation { | ||
| return 0, cerrors.New(cerrors.InvalidArgument, "invalid Marker") | ||
| } | ||
|
|
||
| return n, nil | ||
| return 0, kmsdriver.NewAPIError(kmsdriver.ExInvalidMarker, cerrors.InvalidArgument, | ||
| "The request was rejected because the marker that specifies where pagination should next begin is not valid.") | ||
| } | ||
|
|
||
| // validateMarkerConstraint enforces the MarkerType length and pattern, with | ||
| // the message AWS returns for a failed constraint. | ||
| func validateMarkerConstraint(marker string) error { | ||
| const prefix = "1 validation error detected: Value '%s' at 'marker' failed to satisfy constraint: " | ||
|
|
||
| if utf8.RuneCountInString(marker) > maxMarkerLen { | ||
| return cerrors.Newf(cerrors.InvalidArgument, | ||
| prefix+"Member must have length less than or equal to %d", marker, maxMarkerLen) | ||
| } | ||
|
|
||
| for _, r := range marker { | ||
| if r < 0x20 || r > 0xFF { | ||
| return cerrors.Newf(cerrors.InvalidArgument, | ||
| prefix+"Member must satisfy regular expression pattern: [\\u0020-\\u00FF]*", marker) | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MarkerType constraint check runs after
h.kms.ListGrants, so a 1025-character Marker with a KeyId that doesn't exist gives NotFoundException. Real KMS validates the request shape first and returns ValidationException. The constraint check needs no state, so it could run before the provider call here and in the other handlers that look up a key.