Skip to content

feat(#1273): the CloudWatch Logs tagging trio, and the ARN it takes - #1281

Merged
scttfrdmn merged 1 commit into
mainfrom
feat/1273-logs-tagging
Sep 26, 2026
Merged

scttfrdmn merged 1 commit into
mainfrom
feat/1273-logs-tagging

Conversation

@scttfrdmn

Copy link
Copy Markdown
Owner

TagResource was not mis-validating its resourceArn — it was absent, along with
UntagResource and ListTagsForResource, so a request was refused as an unknown action rather than
accepted with the wrong ARN. That is the correction #1274's audit made to the issue as filed, and it
is why the trio arrives together with the rule: a rule on an unroutable operation is untestable, and
an operation without the rule is the fake that caused the report.

The rule

The three operations take the unsuffixed log-group ARN and refuse the IAM-policy form:

arn:aws:logs:us-west-2:123456789012:log-group:/aws/lambda/foray-gateway       accepted
arn:aws:logs:us-west-2:123456789012:log-group:/aws/lambda/foray-gateway:*     ValidationException: Invalid resourceArn

The trap is that substrate hands a caller the refused form itself: DescribeLogGroups reports both,
and the suffixed one travels under the shorter member name arn. Reaching for it is the natural
mistake — it is also the ARN an IAM policy wants — and a hand-written fake that accepts any ARN keeps
a tagging path green offline and then fails on every group of a live deploy.

The rule is applied to the read as well as to the two writes, because a convergence path reads
before it writes: a ListTagsForResource that accepted the suffixed form would report a group
untagged rather than naming the ARN to send. A :log-stream: suffix is refused the same way.

Provenance

ValidationException / Invalid resourceArn is observed real-AWS behavior (us-west-2, by the
reporter), not published: the pages for all three operations list InvalidParameterException/400,
ResourceNotFoundException/400 and ServiceUnavailableException/500, plus TooManyTagsException/400
on TagResource, and none lists a ValidationException at all.

What is published supports refusing — resourceArn carries Pattern [\w+=/:,.@-]*, a
character class with no * in it, so the suffixed form violates the request model before any
resource is resolved. Only the code and the message text come from observation.

What else lands

  • CreateLogGroup's inline tags are persisted and read back by ListTagsForResource. They
    were decoded and discarded, so a group created with tags inline reported none — the half of the
    convergence path that looked like it worked.
  • tags is a string-to-string map on both the request and the response, not the {key, value}
    array most services use; an untagged group answers {"tags":{}} rather than omitting the member,
    so a caller comparing maps never has to tell nil from empty. Getting this shape wrong is the Logs: DescribeLogGroups/DescribeLogStreams/GetLogEvents return PascalCase members, so an SDK parses every field to null #528
    failure mode for this service: a JSON-1.1 member that does not match the model parses to nothing
    rather than erroring.
  • The 50-tag ceiling is enforced at TagResource, against the merged set — a one-tag request
    against a group holding fifty is refused, while the request alone is within bounds.
  • A destination: ARN answers ResourceNotFoundException. It is the other type all three pages
    publish as taggable, and substrate models no destinations, so the ARN names a taggable type and a
    resource that cannot exist here: the resource is missing, not the ARN wrong.
  • The account and Region come from the ARN, never from the caller's context (SQS state keys disagree: the tagging API and the authorizer address queue:<name>, the plugin writes queue:<account>/<name> #826). The three
    handlers are the only ones in the plugin that take no *RequestContext at all, so there is no
    caller's account in scope for a later edit to reach for.

Deliberately not here

Also pinned, not changed

DescribeLogGroups filters logGroupNamePrefix as a prefix, so a caller checking existence that
way reads /aws/lambda/foray-gateway as existing when only /aws/lambda/foray-gateway-v2 does. That
is faithful, and the issue's closing note asked for it to stay; there is now a test so a well-meaning
edit cannot turn it into an exact match and hide the trap instead of modelling it.

Verification

  • make lint — 0 issues. make test (race, -count=1) — green.
  • make docs-reference-check docs-versions version-check discarded-unmarshal-check wire-bookkeeping-check — green, with the ratchet still at 330: the new Tags member is not a
    bookkeeping field and the record deliberately carries no EverTagged.
  • Patch coverage complete — the diff intersected against the coverage profile leaves no uncovered
    changed statement, including both storeLogGroup failure paths, reached through an injected
    failing store.

Closes #1273. Refs #1274.

TagResource was not mis-validating its resourceArn — it was absent, along with
UntagResource and ListTagsForResource, so the request was refused as an unknown
action. The trio therefore arrives with the rule the issue was filed for: the
operations take the unsuffixed log-group ARN and refuse the IAM-policy form with
the trailing ":*".

Substrate hands a caller the refused form itself. DescribeLogGroups reports both
ARNs and the suffixed one travels under the shorter member name `arn`, so
reaching for it is the natural mistake — and a fake that accepts any ARN keeps a
tagging path green offline and then fails on every log group of a live deploy.

The rule applies to the read as well as the two writes, because a convergence
path reads before it writes: a ListTagsForResource that accepted the suffixed
form would report a group untagged rather than naming the ARN to send.

ValidationException / Invalid resourceArn is observed rather than published — no
page for the three lists a ValidationException at all — but what is published
supports refusing: resourceArn carries Pattern [\w+=/:,.@-], a class with no "*"
in it, so the suffixed form violates the request model before any resource is
resolved. Only the code and message text come from observation.

CreateLogGroup's inline tags are now persisted and read back; they were decoded
and discarded, which is the half of the convergence path that looked like it
worked. The account and Region come from the ARN and never from the caller's
context (#826), which is why the three handlers are the only ones in the plugin
that take no request context at all.

Closes #1273. Refs #1274.
@codecov

codecov Bot commented Sep 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@scttfrdmn
scttfrdmn merged commit 853062a into main Sep 26, 2026
18 checks passed
@scttfrdmn
scttfrdmn deleted the feat/1273-logs-tagging branch September 26, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CloudWatch Logs: TagResource should reject a log-group ARN with a trailing ':*'

1 participant