feat(client): support scoped service-account API tokens via the api.atlassian.com gateway - #53
Merged
Conversation
…is set A scoped API token -- the kind an Atlassian service account gets -- is rejected with a 401 against the site domain and must go through https://api.atlassian.com/ex/confluence/{cloudId} instead. Basic auth already works with such a token, so this is purely a URL change. Split the client's single base in two: baseURL is where requests go (the gateway when --cloud-id / CONFLUENCE_CLOUD_ID is set, else the site) and siteURL is always the Confluence site. Anything a reader sees resolves against siteURL -- printed page URLs and, critically, the base handed to convert.MdToConfluence, since rewritten links are published into the page itself and would otherwise carry the gateway host permanently. The path suffixes are identical under the gateway, so no call site changes. With no cloud ID both bases are the site and behavior is unchanged, which is what an unscoped personal token and any Data Center site need. New and Resolve take Config/Options structs rather than growing another positional string argument; the fields are URL-ish or secret and would transpose too easily unnamed. resolveNext replaces the raw baseURL + _links.next concatenation. Appending is correct -- next is a site-relative absolute path, and url.ResolveReference would silently drop the /ex/confluence/{cloudId} segment -- but it now also guards the converse, where an echoed prefix would be doubled. Verified with info through the gateway: _links.base returns the site URL, so the existing _links.base-preferring logic needs no inversion, and v1 endpoints work there too (author name lookup succeeded). Refs #52
Add a "Scoped tokens and service accounts" section covering why a scoped token needs CONFLUENCE_CLOUD_ID, how to find the cloud ID, and the scopes markfluence requires. Note in the GitHub Actions section that the cloud ID belongs in a repository variable rather than a secret -- it isn't sensitive. Refs #52
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lets markfluence authenticate with an Atlassian scoped API token — the kind a service account gets — so publishing from CI runs as the service account rather than as whichever individual's personal token is in the secret store.
Fixes #52.
This is a URL change, not an auth change
Basic auth already works with a scoped token. What fails is the URL: a scoped token is rejected with a 401 against the site domain and must go through
https://api.atlassian.com/ex/confluence/{cloudId}. The path suffixes are identical under the gateway, so no call site ininternal/clientneeded touching.Why the client now carries two bases
The gateway URL can't simply replace
CONFLUENCE_URL, because the site URL is still needed for things that aren't API requests:convert.MdToConfluencebakes the base into rewritten doc links, so a wrong value publishesapi.atlassian.comlinks into the page — a silent defect that persists long after the run.update/create/fix/info.So
BaseURL()is now strictly where requests go, andSiteURL()is always the site. Both converter call sites carry a comment saying which one and why.Configuration
.env--cloud-idCONFLUENCE_CLOUD_IDWith no cloud ID, both bases are the site and behavior is unchanged — that's the back-compat guarantee for existing personal-token users and for Data Center, which has no cloud ID at all. A URL-ish cloud ID is rejected at config time, since pasting a whole gateway URL would otherwise surface as an opaque 404.
New/Resolvemoved toConfig/Optionsstructs rather than growing a fourth positional string.resolveNextreplaces the rawbaseURL + _links.nextconcatenation inListContentProperties. Appending is the correct behavior (nextis a site-relative absolute path, andurl.ResolveReferencewould silently drop/ex/confluence/{cloudId}), but it now also guards the converse case where an echoed prefix would be doubled.Verified against the live API
Gateway routing accepts an ordinary personal token, so
infowas testable end-to-end before the service account exists. Both open questions from the design notes are answered:_links.basereturns the site URL through the gateway (https://<site>.atlassian.net/wiki), so the existing_links.base-preferring logic needs no inversion.infooutput is identical with and without--cloud-id./wiki/rest/api/usercall.Still unverified
The v1 attachment upload. Atlassian documents classic scopes for v1 vs granular for v2 while warning against mixing sets, so a scope gap would surface there, and it's load-bearing for any page with images. A personal token is unscoped, so testing it now would prove nothing about scopes — this has to wait for the real credential. First real run should be
update --dry-runon a page with an image.Deliberately out of scope
client_credentials(2LO) and so is CI-usable, but theclient_secretthat mints the 60-minute token is itself long-lived — two secrets, same blast radius — and it needs this same gateway change anyway.Test notes
Covered:
resolveNext(all branches), the two bases, cloud-ID precedence and validation, andpageURLin gateway mode.Not covered: the one-line
MdToConfluence(mf, c.SiteURL(), ...)wiring. An end-to-end assertion would require making the gateway prefix injectable so a test server could stand in forapi.atlassian.com— more machinery than the risk seemed to warrant, but happy to add it.Design notes in
_plans/017_gateway-scoped-tokens.md.