Skip to content

feat(ENG-13683): add Maven credential helper via shell plugin - #339

Draft
cloudsmith-iduffy wants to merge 11 commits into
credential-genericfrom
maven-shell-plugin
Draft

feat(ENG-13683): add Maven credential helper via shell plugin#339
cloudsmith-iduffy wants to merge 11 commits into
credential-genericfrom
maven-shell-plugin

Conversation

@cloudsmith-iduffy

@cloudsmith-iduffy cloudsmith-iduffy commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Description

Stack 4/4 — the Maven helper itself. Base: #338; the custom-domain work it
relies on is #337.

Maven has no credential-helper protocol, so this authenticates it with a shell
plugin instead. credential-helper install maven --org <org> --repo <repo>
writes an mvn shim that wraps every invocation in cloudsmith exec, which
injects an ephemeral mode-0600 settings.xml via mvn -s and deletes it when
the run ends — no token is ever written to durable configuration.

  • credential-helper shell-init prints the shell initialisation (bash, zsh,
    fish) that puts the shims directory first on PATH.
  • cloudsmith exec -- <command> is callable directly, for CI that should not
    touch PATH.
  • Publishing is opt-in: install prints the distributionManagement snippet to
    add to pom.xml.
  • Custom download/upload domains are discovered from the organisation. A domain
    bound to the repository being installed beats an organisation-wide one, and
    one bound to a different repository is never used. Which kind each host is
    gets recorded in package-managers.ini, so wrapped runs need no lookup — the
    run path makes no API call and no cache read.

Credential binding. Maven matches a <server> to a repository by id alone,
with no host check, and the shim applies to every mvn run in every directory.
A fixed, guessable server id would therefore let any checked-out pom.xml
declaring a repository under that id receive the token on an ordinary
mvn compile. So a wrapped run binds the injected download credential to a
random id generated fresh per invocation, and the stable id your
distributionManagement names is supplied only when the invocation is actually
a deploy.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactoring
  • Other (please describe)

Additional Notes

Test suite: 775 passed, 40 skipped at this commit.

Two things a reviewer should know:

  • Wrapped runs do not consult ~/.m2/settings.xml; mirrors, proxies and other
    <server> entries are not seen. Passing your own -s/--settings runs Maven
    unwrapped, with a warning. This is the designed mechanism — Maven has no
    settings-merge — and is called out at install time and in the CHANGELOG.
  • One residual exposure ships knowingly: the stable server id defaults to the
    literal cloudsmith, so on a deploy a hostile checkout can still claim it
    during dependency resolution. Minting it as cloudsmith-<random> at install
    time would close it; that changes a default users paste into pom.xml, so it
    is deliberately left to its own change. Documented in build_settings_xml's
    docstring.

cloudsmith-iduffy and others added 11 commits August 1, 2026 14:18
Maven has no credential-helper protocol, so authenticate it with a shell
plugin instead: `credential-helper install maven --org <org> --repo <repo>`
writes an `mvn` shim that wraps every invocation in `cloudsmith exec`, which
injects an ephemeral mode-0600 settings.xml via `mvn -s` and deletes it when
the run ends. No token is ever written to durable configuration.

`credential-helper shell-init` prints the shell initialisation that puts the
shims directory first on PATH. `cloudsmith exec -- <command>` is callable
directly for CI, without touching PATH. Publishing is opt-in: install prints
the distributionManagement snippet to add to pom.xml.

Tests are split by subject from the outset — _plugin (binding, settings.xml,
shims, shell-init), _runner (exec) and _installer (install/uninstall plus CLI
wiring) — since one module covering all of it would exceed the 1000-line
limit pylint enforces here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolve the Maven-side findings from a review of the credential helper:

- Discard a stored binding's repo and hosts when --org names a different
  organisation, instead of pasting the old repo slug under the new org.
- Detect Maven's attached -s<file> form, which defeated the guard against
  shadowing a user's own settings.xml.
- Point shims at sys.executable for frozen builds, which are not on PATH
  under the name `cloudsmith`.
- Probe the platform's actual shim filename, so status and uninstall find
  mvn.cmd on Windows.
- Route each --domain value to the download or upload host by its discovered
  backend kind, reporting a superseded value rather than dropping it.
- Strip CLOUDSMITH_ORG/CLOUDSMITH_REPO, which click passes through verbatim,
  so a padded value cannot become a slug.

The installer's custom-domain fixtures also carry the organisation the
record was discovered under, which the domain layer now requires.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Record on the Maven bullet: a domain bound to the repository being installed
is preferred over an organisation-wide one and a domain bound to a different
repository is never used; which kind each host is gets recorded alongside it
so wrapped runs need no lookup; a repository-scoped domain's URLs carry
neither an organisation nor a repository segment; and `exec --repo <other>`
resets such a host rather than reusing it for an unrelated repository.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…host

repo_path_segment decided whether to include <org> by calling
is_standard_cloudsmith_host(), which only recognises *.cloudsmith.io/.com. A
host declared in a trusted [domains] table is a deployment's own service
host - the on-prem/dedicated equivalent of dl.cloudsmith.io - not a
genuinely discovered custom domain, so it must be org-qualified the same
way. Previously it took the custom-domain branch and dropped the org,
resolving every wrapped mvn run on such a deployment against the wrong path.

Adds is_default_host() in common.py, checking the effective default-domain
table (load_default_domains(), builtins or a trusted override) rather than
hostname suffixes. is_standard_cloudsmith_host() is unchanged - it still
decides token eligibility in is_cloudsmith_domain() - and repo_path_segment
now honours either check. Reading config.ini from disk is fine on the
provision() run path (matching default_cdn_host() already doing so); no
network call is introduced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- shellplugin/maven.py: create settings.xml 0600 via os.open(O_CREAT|
  O_EXCL|O_WRONLY) instead of open() + chmod, closing the brief window at
  the process umask.
- maven/installer.py: _select_domain sorts candidates by host before
  picking, so choosing among several organisation-scoped domains no longer
  depends on the discovery API's return order.
- maven/installer.py: install() now also warns when `cloudsmith` itself is
  unresolvable (and the build is not frozen) - the shim execs `cloudsmith
  exec -- mvn`, so an inactive venv breaks every wrapped mvn run, not just
  discoverability of the shims dir.
- maven/installer.py: _deploy_snippet notes that `exec --org`/`--repo`
  overrides retarget downloads only; `mvn deploy` still publishes to the
  pom's distributionManagement, written for the install-time repository.
- maven/installer.py: _select_domain/_is_eligible's repo parameter is now
  annotated str | None, matching _resolve_hosts's caller annotation.
- shellplugin/maven.py: download_url/upload_url parameters are now
  annotated, matching the rest of the module.
- Added direct coverage for _drop_repository_scoped_hosts's upload_host/
  upload_scope branch (previously unobservable through `exec`, which only
  injects cdn_host into settings.xml) and for an explicit `exec --repo`
  matching the already-stored repo leaving a repository-scoped host intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tion id

Maven matches a <server>'s credentials to a repository by id alone, with
no host check, and the injected settings.xml used the fixed, guessable
"cloudsmith" default id for every wrapped run - so any checked-out
project whose pom.xml declared a repository under that id received the
live token on an ordinary `mvn compile`. The download profile/repository/
server we author ourselves now get a fresh secrets.token_hex id every
provision() call, and the stable registry_id server that matches the
user's distributionManagement is emitted only for deploy invocations
(deploy, deploy:*, release:perform), since that is the only case that
needs it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dinates

Ruled by the project owner: mvn org.apache.maven.plugins:maven-deploy-plugin:3.1.1:deploy
is the same deploy goal as `deploy:deploy`, spelled out in full rather than
via shorthand, so it must also unlock the stable distributionManagement
server - matching on args ending in ":deploy" catches it while a goal that
merely contains the word (deploy-helper:run, -Dmaven.deploy.skip=true)
still does not count. Under-matching breaks publishing with an invisible
401; over-matching only emits the stable server on a goal the user
explicitly aimed at publishing, so the asymmetry favours widening.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The injected download credential now carries an unguessable per-invocation id,
but the stable server emitted for deploys still uses the default `cloudsmith`
id, so a hostile checkout can claim it during a deploy's dependency
resolution. Note the exposure and the fix for it where the next maintainer
will be looking, rather than leaving it in a review thread.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

assert config.get_plugin("maven").cdn_host == "second.cdn.example.com"
assert any(
a.startswith("WARNING") and "first.cdn.example.com" in a for a in actions
@cloudsmith-iduffy cloudsmith-iduffy changed the title maven shell plugin feat(ENG-13683): add Maven credential helper via shell plugin Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants