Skip to content

feat: read Assets object type attributes, and select the Jira API version on the passthrough - #76

Merged
Hinne1 merged 11 commits into
mainfrom
claude/assets-attributes-api-version
Sep 18, 2026
Merged

Hinne1 merged 11 commits into
mainfrom
claude/assets-attributes-api-version

Conversation

@Hinne1

@Hinne1 Hinne1 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

What

Two read-only additions, both driven by questions the CLI could not answer.

atl jira assets attributes <object-type-id> (alias fields) lists the
attributes an Assets object type defines, with their numeric ids. An Assets
object omits every attribute it holds no value for, so reading one object can
never show whether an attribute exists on its type at all; this reads the type
definition instead. The object type's name is printed above the table, because
a wrong id and a type that genuinely lacks an attribute otherwise look the
same.

atl jira api --api-version <2|3> lifts the passthrough off its hardcoded
/rest/api/3. Both versions expose the same resources and differ in how they
carry rich text, so reading through v2 returns a description as wiki markup
instead of ADF.

Why

Deciding whether a Jira Assets object type carries a given attribute came up
while verifying a sync that writes to Assets, and neither AQL nor a single
object read can answer it. AQL resolves attribute names workspace-wide in both
WHERE and ORDER BY, so a type-scoped probe returning zero rows proves
nothing, and an object without a value for an attribute is indistinguishable
from an object whose type lacks it.

New OAuth scopes

Assets grants reads per resource kind. GET /objecttype/{id} needs
read:cmdb-type:jira and GET /objecttype/{id}/attributes needs
read:cmdb-attribute:jira; the object and schema scopes the CLI already
requests cover neither. Both are added to DefaultScopes() and checked before
the request, so a token without them fails with the missing scope named rather
than as an opaque 401 "scope does not match" from Atlassian.

This needs action outside the repo before the command works: the atl-cli
OAuth app must list the two scopes in the developer console, and each site
needs a fresh login afterwards. Existing tokens do not gain scopes
retroactively. Consent on enthus.atlassian.net is site-admin gated.

Security

The version segment is interpolated into the URL, which validateRawPath
never inspects, so JiraBaseURLVersion validates it against an allowlist
inside the API layer. The flag-level check is a convenience on top of that, not
the guarantee. TestJiraBaseURLVersion covers 3/../../.. and ../agile/1.0
among others, and fails if the allowlist check is neutered (verified by
mutation). The passthrough stays GET-only.

Testing

make check    # fmt, vet, golangci-lint, full test suite — exit 0

New tests: TestJiraBaseURLVersion, TestJiraBaseURLMatchesDefaultVersion,
TestRawGetVersionRejectsUnsupportedVersion, TestNewCmdAPI_APIVersionFlag,
TestAssetsObjectType, TestAssetsObjectTypeAttributes,
TestAssetsObjectTypeReadsNeedTypeAndAttributeScopes, TestAttributeFlags.

Run against both live sites with a binary built from this branch. Every
objecttype/* endpoint returns 401 "scope does not match" on prod and
sandbox alike, which is the scope gap above, not a defect in these commands;
with the pre-check in place the CLI now reports the missing scope by name and
points at the re-login. The endpoints the current scopes do cover
(objectschema/list, objectschema/{id}, the workspace lookup) still return
200, so the wall is specific to the object type resources.

The passthrough was bound to /rest/api/3. Add --api-version so v2 is
reachable: both versions expose the same resources and differ in how they
carry rich text, so reading through v2 returns a description as wiki markup
instead of ADF.

The version is interpolated into the URL, which the path-level guard never
sees, so it is validated against an allowlist inside the API layer rather than
only at the flag.
An Assets object omits every attribute it holds no value for, so reading one
object cannot show whether an attribute exists on its type at all. Add
'jira assets attributes <object-type-id>' (alias 'fields'), which reads the
type definition instead, and prints the type's name above the table so a wrong
id is distinguishable from a type that genuinely lacks the attribute.

Assets grants reads per resource kind: the two endpoints need
read:cmdb-type:jira and read:cmdb-attribute:jira, which the existing object and
schema scopes do not cover. Both are added to the requested scopes and checked
before the request, so a token without them fails with the scope named rather
than an opaque 401 "scope does not match".
Also splits the Assets scopes onto their own line in the OAuth app setup, now
that there are four of them.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request adds support for listing Jira Assets object types and their attributes, including new CLI commands and API client methods. It also introduces the ability to select Jira API versions (v2 or v3) for the api passthrough command. The reviewer suggested implementing a TypeName() helper method on AssetObjectTypeAttribute to correctly label non-default attribute types, updating the CLI output to use this method, and adding corresponding unit tests.

Comment thread internal/api/assets.go
Comment thread internal/cmd/assets/attributes.go
Comment thread internal/api/assets_test.go
…g cardinality

The type and its attributes are gated by different scopes, so a token holding
one but not the other failed on the second request only after the first was
fixed. Check both before either request.

The multi-value label read every upper cardinality other than 1 as unbounded,
which labels an absent or zero field as multi-valued. Assets spells unbounded
as a negative number, so require that form or a stated bound above one.
The contract of the combined pre-check is that one message lists every gap; a
per-call check would have satisfied the type before failing on the attributes.
A Select rejects any value outside its option list, so the options are what a
caller has to match when writing the attribute. Reading the type without them
answers that the attribute exists but not what may be put in it.
Only a Default attribute carries its kind in defaultType; a reference, user,
group, or project attribute leaves it empty and rendered as a blank column.
Fall back to the numeric type, reported verbatim because Assets does not
publish that enum and a guessed label would be worse than a number.

Also corrects the options doc: a Text attribute can carry a predefined value
list too, so options are not confined to Select.
…on a bad version

Any negative upper cardinality read as unbounded; -1 is the only negative a live
workspace produces, so an unobserved negative now reads as no flag rather than
as a claim about a sentinel whose meaning Assets does not publish.

The version allowlist ran only inside the API layer, after the authenticated
client was built, so an unsupported version surfaced behind an auth error for
anyone not logged in. The URL-building guard stays; this is the message.
@Hinne1

Hinne1 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

…le to the API

The id is interpolated into the request path and url.PathEscape leaves ";"
intact, which is enough to append a path parameter and change how the server
parses the request. The passthrough already blocks that class for Jira paths;
the Assets reads had no equivalent guard. An id is always digits, so an
allowlist closes it without chasing encodings.

The scope union is now derived from the per-call requirements instead of
restating them, so the up-front check cannot advertise less than the calls
demand. IsMulti moves next to Required and TypeName, leaving the command with
presentation only. Both reads wrap their error so a failure names which one.

RawGet had no callers left and is removed rather than kept as a second entry
point to the same URL construction. ObjectCount and Inherited lose omitempty,
which was hiding a zero count and a false inherited from JSON consumers.
@Hinne1

Hinne1 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for querying Jira Assets (CMDB) object types and their attributes, including a new atl jira assets attributes command. It also enhances the read-only REST passthrough (atl jira api) command by adding an --api-version flag to select between Jira platform REST API versions 2 and 3. Feedback suggests executing the independent network requests for fetching object type metadata and attributes concurrently to reduce latency and improve responsiveness.

Comment thread internal/cmd/assets/attributes.go

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for listing Jira Assets (CMDB) object types and attributes via a new 'attributes' command, updates the read-only REST passthrough to support selecting the Jira API version (v2 or v3), and adds corresponding validation, documentation, and tests. Feedback on the changes suggests running the object type and attribute retrieval concurrently in the new command to reduce latency (while pre-resolving the workspace ID to prevent a data race), and fixing a resource leak in 'TestObjectTypeIDMustBeNumeric' where a test server is not closed.

Comment on lines +74 to +81
objectType, err := client.ObjectType(cmd.Context(), args[0])
if err != nil {
return fmt.Errorf("read object type %s: %w", args[0], err)
}
attributes, err := client.ObjectTypeAttributes(cmd.Context(), args[0])
if err != nil {
return fmt.Errorf("read attributes of object type %s: %w", args[0], err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The client.ObjectType and client.ObjectTypeAttributes calls are currently executed sequentially. Since they are independent, they can be executed concurrently to significantly reduce command latency.

Additionally, because AssetsClient does not protect its workspaceID field with a mutex, calling these two methods concurrently when the workspace ID is not yet cached would introduce a data race. Pre-resolving the workspace ID sequentially first avoids this data race while still allowing the actual API requests to run in parallel.

			// Pre-resolve workspace ID sequentially to avoid a data race in the client
			if _, err := client.WorkspaceID(cmd.Context()); err != nil {
				return err
			}

			type typeResult struct {
				val *api.AssetObjectType
				err error
			}
			type attrsResult struct {
				val []api.AssetObjectTypeAttribute
				err error
			}

			typeChan := make(chan typeResult, 1)
			attrsChan := make(chan attrsResult, 1)

			go func() {
				val, err := client.ObjectType(cmd.Context(), args[0])
				typeChan <- typeResult{val, err}
			}()

			go func() {
				val, err := client.ObjectTypeAttributes(cmd.Context(), args[0])
				attrsChan <- attrsResult{val, err}
			}()

			tRes := <-typeChan
			if tRes.err != nil {
				return fmt.Errorf("read object type %s: %w", args[0], tRes.err)
			}
			objectType := tRes.val

			aRes := <-attrsChan
			if aRes.err != nil {
				return fmt.Errorf("read attributes of object type %s: %w", args[0], aRes.err)
			}
			attributes := aRes.val

Comment on lines +236 to +240
func TestObjectTypeIDMustBeNumeric(t *testing.T) {
client := newTestAssetsClient(httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
t.Error("a rejected id reached the network")
w.WriteHeader(http.StatusOK)
})), "workspace-456")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The test server created by httptest.NewServer is passed directly to newTestAssetsClient without being assigned to a variable, which prevents it from being closed. This leaks the test server and its background listener. Assign the server to a variable and defer server.Close() to clean up resources properly.

func TestObjectTypeIDMustBeNumeric(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		t.Error("a rejected id reached the network")
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()
	client := newTestAssetsClient(server, "workspace-456")

@Hinne1

Hinne1 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

🔏 Readiness attested — the pr-readiness-check gate completed for PR #76 at 64576875656594a54b7344ebd9c6f1905586ac38 (2026-09-18T07:17:20Z). If the branch head has moved, this evidence covers the earlier state only.

Readiness summary

PR Readiness Check (#76, 6457687)
Review mode INITIAL
Branch/base PASS
Local verification PASS
Remote CI PASS (CodeQL, Analyze go, Analyze actions) /
DEFERRED UNTIL READY (Build, Lint, Test, Coverage)
Coverage NOT REPORTED (Coverage Report skips while draft)
Automated reviewers PASS (Gemini reviewed current head; Claude review not
applicable, repository has no claude-review.yml)
Review threads PASS (0 unresolved)
Council findings DISPOSITIONED (3 rounds, jury degraded in all three)
Standards review DISPOSITIONED (covered within the Council rounds)
Spec review DISPOSITIONED (covered within the Council rounds)
PR description PASS
SQL/UI evidence SKIP (no SQL, no UI)
Result: READY

Verification

make check (gofmt, go vet ./..., go tool golangci-lint run, full test
suite) exits 0 on the attested head, re-run after every mutation. Build, Lint,
Test and Coverage skip on a draft PR by workflow condition, so they are deferred
to the ready transition rather than counted as passing.

Verified live against both Atlassian sites with a binary built from this branch:
sandbox object type 14 and prod object type 9 both read as Mitarbeiter, each
carrying a Status Select with options aktiv,inaktiv. Sandbox type 9 returns
404 while prod type 9 exists with different attribute ids, which confirms the
two sessions address different workspaces.

Dispositions

Council: 3 rounds, 12 findings FIXED, 4 REFUTED, 4 DEFERRED, 3 ADVISORY. No
Critical and no Important in any round. Two adopted findings were substantive: a
path-parameter injection gap on the object type id, which url.PathEscape does
not close because it leaves ; intact, now allowlisted to digits; and the scope
union, now derived from the per-call requirements and pinned by a test.

Gemini: reviewed the current head. Its blank-TYPE-column finding was confirmed
against live payloads and fixed; its suggested enum mapping was refuted with
measurements from the prod workspace showing the labels are offset, so adopting
it would have rendered Jira User as Group. Its concurrency suggestion is
deferred with reasoning posted on the thread.

Three refutations were reviewers contradicted by the file itself: two Council
reviewers plus the mediator claimed objectType.Name is printed unsanitized
when it has gone through terminalText since the command was written, and round
3 called the scope union "a separate literal slice" while reviewing a diff
containing slices.Concat(objectTypeScopes, objectTypeAttributeScopes).

Full detail in council-dispositions.md and verification.md in this directory.

Disclosed limitations

The Council jury ran degraded in all three rounds, returning 2 of 3 usable
juror verdicts with github-gpt erroring each time. Reviewer participation was
5 of 22, 7 of 22 and 7 of 22. Findings were dispositioned on their evidence
rather than on the raw verdict, which was REVISE in every round and carried no
Critical or Important finding at any point.

This change grows the OAuth scopes every login requests by read:cmdb-type:jira
and read:cmdb-attribute:jira. Both are already granted on the atl-cli app
registration, and both sites have been re-authenticated, so the new command
works today. Anyone else holding an older token keeps assets count, aql and
object working and must re-login for the object type reads. This is documented
in the README and is a deployment consequence, not a defect.

@Hinne1
Hinne1 marked this pull request as ready for review September 18, 2026 07:17
@github-actions

Copy link
Copy Markdown

Merging this branch will increase overall coverage

Impacted Packages Coverage Δ 🤖
github.com/enthus-appdev/atl-cli/internal/api 43.40% (+1.12%) 👍
github.com/enthus-appdev/atl-cli/internal/auth 32.63% (ø)
github.com/enthus-appdev/atl-cli/internal/cmd/assets 19.86% (+6.00%) 👍
github.com/enthus-appdev/atl-cli/internal/cmd/jira 48.72% (+4.27%) 👍

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/enthus-appdev/atl-cli/internal/api/assets.go 66.32% (+10.07%) 95 (+31) 63 (+27) 32 (+4) 🎉
github.com/enthus-appdev/atl-cli/internal/api/client.go 50.00% (+1.65%) 188 (+6) 94 (+6) 94 👍
github.com/enthus-appdev/atl-cli/internal/api/jira.go 28.39% (+0.39%) 560 (+3) 159 (+3) 401 👍
github.com/enthus-appdev/atl-cli/internal/auth/oauth.go 1.27% (ø) 79 1 78
github.com/enthus-appdev/atl-cli/internal/cmd/assets/assets.go 0.00% (ø) 16 (+1) 0 16 (+1)
github.com/enthus-appdev/atl-cli/internal/cmd/assets/attributes.go 35.90% (+35.90%) 39 (+39) 14 (+14) 25 (+25) 🌟
github.com/enthus-appdev/atl-cli/internal/cmd/jira/api.go 61.29% (+4.15%) 31 (+3) 19 (+3) 12 👍

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

Changed unit test files

  • github.com/enthus-appdev/atl-cli/internal/api/assets_test.go
  • github.com/enthus-appdev/atl-cli/internal/api/client_test.go
  • github.com/enthus-appdev/atl-cli/internal/api/jira_test.go
  • github.com/enthus-appdev/atl-cli/internal/cmd/assets/attributes_test.go
  • github.com/enthus-appdev/atl-cli/internal/cmd/jira/api_test.go

@Hinne1
Hinne1 merged commit c52b031 into main Sep 18, 2026
15 checks passed
@Hinne1
Hinne1 deleted the claude/assets-attributes-api-version branch September 18, 2026 08:14
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.

1 participant