Skip to content

Restrict what a repository-resolved model contributes to the build - #12948

Open
slachiewicz wants to merge 14 commits into
apache:masterfrom
slachiewicz:pr/model-building-master
Open

Restrict what a repository-resolved model contributes to the build#12948
slachiewicz wants to merge 14 commits into
apache:masterfrom
slachiewicz:pr/model-building-master

Conversation

@slachiewicz

@slachiewicz slachiewicz commented Aug 30, 2026

Copy link
Copy Markdown
Member

A model resolved from a repository should contribute less to the build than the project's own POM.

  • Repository precedence. Repositories contributed by a resolved model merge recessively; those supplied by the request or session keep precedence. BUILD_* requests — the project being built — are unaffected.
  • Profile activation. For models resolved while building a dependency POM, profiles activated by file, property or condition are skipped, and profiles that do activate contribute no repositories. Settings and CLI profiles are untouched.
  • Parent cache partitioning. The resolved-parent cache is keyed by resolution origin, so a parent reached from a dependency chain and the same parent reached from the project's own chain no longer share an entry.
  • Property interpolation. A model built while resolving a dependency POM or its parent resolves, from the session properties, only an environment-independent set of expressions: java.*, os.*, maven.*, the three separators, and the CI-friendly revision, changelist and sha1. Everything else, environment variables included, is left as a literal expression. maven.model.dependencyInterpolation.full=true restores the previous behaviour.
  • system scope in imported dependency management. Dependency management imported from a POM resolved from a repository no longer contributes entries that declare system scope or a systemPath; those entries are dropped and a warning names them. Management imported from the reactor is unaffected, and so is a system scope declared by the project's own POM. maven.repository.dependencyManagement.allowSystemScope=true restores the previous behaviour.

The last two change behaviour that has been in place for a long time, which is why each ships an opt-out property. MavenITmng4379, MavenITmng4590 and MavenITmng3586 keep their original assertions behind the opt-out and each gains a companion test asserting what the new default does instead. MavenITmng3586#testitFromProject, where the project's own POM declares the system scope, needs no opt-out.

Worth a maintainer's eye on the interpolation default in particular: MNG-4379 has resolved a systemPath from an environment variable in a repository-served POM since 2009, and under the new default such a build fails on the existing "must specify an absolute path" validation rather than resolving.

Which of these applies is decided by the model's source kind rather than by the request type: a model
built from Sources.resolvedSource(...) is one Maven fetched from a repository and reports a model id,
while a POM handed to Maven as a file reports none. MavenITmng4765LocalPomProjectBuilderTest covers
the latter, and two unit tests assert a caller-supplied model is left alone. Keying on the request type
alone would not work: a POM supplied through the 2.x project builder API arrives with the same type as a
resolved dependency.

Each change is a separate commit.


Draft while related code paths are reviewed.

@slachiewicz
slachiewicz marked this pull request as draft August 30, 2026 19:05
@slachiewicz
slachiewicz force-pushed the pr/model-building-master branch from 3bf1265 to b9f115a Compare August 30, 2026 19:33
Repositories contributed by a model resolved from a repository are merged
recessively; repositories supplied by the request or session keep precedence.
Gates the replace branch of ModelBuilderSessionState.mergeRepositories() on
isBuildRequest(), so only filesystem/project models (BUILD_PROJECT,
BUILD_EFFECTIVE, BUILD_CONSUMER) retain replace semantics.
Models built while resolving a dependency POM or one of its parents from a
repository (ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY and
CONSUMER_PARENT) now interpolate their user/system properties only against a
small, environment-independent allowlist (java.*, os.*, maven.*, the
separator properties, and the CI-friendly revision/changelist/sha1);
everything else, including env.* and arbitrary -D properties, is left as a
literal unresolved expression. Models belonging to the project being built
use the BUILD_* request types and are unaffected. The prior behavior can be
restored globally with the new maven.model.dependencyInterpolation.full
system/user property.
Extends the previous restriction to the deprecated compat model-builder
interpolator (AbstractStringBasedModelInterpolator), which independently
builds its own value-source list. Models built at
ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL are the models Maven builds
while resolving dependency, parent and BOM-import POMs from a repository;
such models now interpolate their user/system properties only against the
same allowlist as the modern builder. Operator project builds use a higher
validation level and are unaffected. Same
maven.model.dependencyInterpolation.full opt-out applies here too.
…pendency POMs

Models built to resolve a dependency (ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY)
are read from a repository, not authored by the project being built. For such models,
file, property, and condition activation conditions are no longer evaluated, and profiles
that do still activate (JDK version, operating system, activeByDefault) no longer
contribute repositories or plugin repositories. Profiles supplied via settings or the
command line, and models belonging to the project's own parent chain, are unaffected.
…s (compat)

Extends the previous restriction to the deprecated compat model-builder
(DefaultModelBuilder), which independently performs its own profile
selection and injection. Models built at
ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL are the models Maven builds
while resolving dependency, parent and BOM-import POMs from a repository.
For such models, file and property activation conditions are no longer
evaluated, and profiles that do still activate (JDK version, operating
system, activeByDefault) no longer contribute repositories or plugin
repositories. Profiles supplied via settings or the command line are
unaffected.
readAsParentModel() caches the parent model it builds keyed only by source and
activation record. A dependency's own remote parent and a project's own remote
parent can be the exact same POM source, and the two must not share that cache
entry: only a request originating from CONSUMER_DEPENDENCY skips file,
property, and condition activation and drops repositories, so a cache hit
computed under one lineage must never be handed to the other.

request.getRequestType() cannot carry that distinction by itself, because a
parent lookup always derives a CONSUMER_PARENT request regardless of what kind
of session triggered it. A new sticky externalOrigin flag is threaded through
derive() instead -- seeded on CONSUMER_DEPENDENCY and OR'd forward through
every derivation, surviving as many parent hops as the lineage has -- and
readAsParentModel() and the shared getActiveProfiles() gate both key off it.
The cache is partitioned into two tags accordingly, so the two lineages never
share an entry for the same source. Internal to ModelBuilderSessionState; no
public API change.
…tory POM

A dependencyManagement import (scope=import or type=bom) reads its managed
dependencies from whichever POM the coordinates resolve to. system scope and
systemPath bind a dependency to a file on the local filesystem, which is only
meaningful for projects belonging to the current build.

Track, in doLoadDependencyManagement, whether the import POM was resolved from
the reactor or from a repository. Managed dependencies declaring system scope or
a systemPath in a repository-resolved import are dropped with a warning, and are
removed from the cached model so a later import cannot reintroduce them. The
previous behaviour remains available, also with a warning, behind the new
maven.repository.dependencyManagement.allowSystemScope user property. Imports
resolved from the local reactor are unaffected.
@slachiewicz
slachiewicz force-pushed the pr/model-building-master branch 2 times, most recently from 6bef3e0 to 9e78b2a Compare August 30, 2026 20:04
MavenITmng4379 and MavenITmng3586 cover a systemPath supplied through a
property or an environment variable in a POM resolved from a repository;
MavenITmng4590 covers dependency management imported from one. Those models
now use restricted defaults, so each test requests the previous behaviour
explicitly, which exercises the opt-out property, and gains a companion test
asserting what the default does instead.

Verified: mvn verify -Prun-its -Dits.test='MavenITmng4379*,MavenITmng4590*,MavenITmng3586*' -> 7 tests, 0 failures
@slachiewicz
slachiewicz force-pushed the pr/model-building-master branch from 9e78b2a to 27e4655 Compare August 30, 2026 20:11
The static fixture pom used a POSIX-only systemPath, which File.isAbsolute()
rejects on Windows; model validation then dropped the imported entry before
the code under test ran, so the test's assertNull passed for the wrong
reason. The test now writes the BOM into a per-test temporary remote
repository with a systemPath computed from the current OS, and asserts that
an ordinary managed dependency from the same import is present, so the
system-scope assertions can no longer pass vacuously. The now-unused static
fixture pom is removed.
DefaultProjectBuilder decided between BUILD_EFFECTIVE and CONSUMER_DEPENDENCY
partly on the request's validation level. Validation strictness says how closely
a model is checked, not whose model it is, and the 2.x project builder API asks
for MAVEN_2_0 validation on a file the caller supplies, so such a build was
labelled a consumer dependency. A non-null pomFile already marks a model backed
by a file the caller pointed at -- a model resolved from a repository arrives as
a resolved source with no pomFile -- so that alone now decides it.

Covered by MavenITmng4765LocalPomProjectBuilderTest, which builds a local file
through that API and reads back a command-line property.
This reverts commit dd8f0ca.

RequestType is read for validation strictness as well as for provenance, so
promoting every file-backed build to BUILD_EFFECTIVE turned lenient builds
strict: DefaultMavenProjectBuilderTest.testPartialResultUponBadDependencyDeclaration,
MavenProjectTest.testCloneWithDependencyManagement and
PomConstructionTest.testDuplicateDependenciesCauseLastDeclarationToBePickedInLenientMode
all depend on the previous behaviour.
The restriction on property interpolation and profile activation for a
consumer-dependency or consumer-parent model now also requires the model
source to be one Maven resolved from a repository (ModelSource.getModelId()
non-null). A POM the caller hands to Maven as a file arrives with the same
request type but is not repository-resolved, so it keeps full interpolation
and activation.

Integration-level cover: MavenITmng4765LocalPomProjectBuilderTest.

@gnodet gnodet 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.

✅ Well-Designed Security Hardening

Well-structured behavioral change with thorough test coverage and clear opt-out mechanisms. This is the master sibling of #12943 (maven-4.0.x), carrying the same logic with consistent implementations across compat and impl paths.

The test coverage is excellent: unit tests cover profile activation filtering, interpolation restriction, cache partitioning, externalOrigin propagation through grandparent hops, caller-supplied model exemption, and the opt-out property; integration tests cover both the new default and the opt-out for MNG-4379, MNG-4590, and MNG-3586.

Two observations (non-blocking, same as 4.0.x review):

  1. Configuration documentation gap (DefaultModelInterpolator.java:67): The maven.model.dependencyInterpolation.full opt-out property is declared as a local constant in two implementation classes, unlike its companion maven.repository.dependencyManagement.allowSystemScope which is in Constants.java with @Config. This means it won't appear in Maven's generated configuration documentation, creating a discoverability gap for users who need the opt-out. Since this PR changes longstanding default behavior (env-var interpolation in repository-resolved POMs, active since MNG-4379 in 2009), the opt-out should be as discoverable as possible.

  2. Future-proofing note (DefaultModelBuilder.java:645): The replace && isBuildRequest() guard in mergeRepositories is correct for all current request types, but any future non-BUILD request type will implicitly receive recessive merging without an explicit decision. A brief code comment noting this design intent would help future maintainers.

Technical notes:

  • The compat and impl implementations are consistent: isSafeExternalExpression is identical; compat correctly omits condition activation filtering (doesn't exist in legacy model builder).
  • externalOrigin propagation through derive() is correctly sticky — a dependency's parent inherits the restriction.
  • The ModelSource.getModelId() != null approach is cleaner than modifying request type classification, and preserves full interpolation for the 2.x project builder API path.

📋 PR Metadata

Aspect Current Suggested
Milestone (none) 4.1.0

🔀 Backport Status

  • maven-4.0.x#12943 (OPEN, reviewed)
  • maven-3.10.x#12953 (OPEN)
  • ℹ️ maven-3.9.x — not targeted (appropriate for behavior-changing enhancement)

🤖 This review was generated by ForgeBot.

gnodet and others added 2 commits August 31, 2026 10:01
Move the maven.model.dependencyInterpolation.full property from local
constants in DefaultModelInterpolator and
AbstractStringBasedModelInterpolator to Constants.java with @config so
it appears in Maven's auto-generated configuration documentation
alongside its companion maven.repository.dependencyManagement.allowSystemScope.

The compat module keeps a private mirror of the value because it does
not depend on maven-api-core.

Also add a design-intent comment on the replace && isBuildRequest()
guard in DefaultModelBuilder.mergeRepositories to clarify that
non-build request types intentionally use recessive merging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update DefaultModelInterpolatorTest to use
Constants.MAVEN_MODEL_DEPENDENCY_INTERPOLATION_FULL and
AbstractModelInterpolatorTest to use the string literal since the
compat constant is now private.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@gnodet gnodet 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.

Delta review — two new commits since the previous review address both observations:

  1. Configuration documentation gap → Fixed. maven.model.dependencyInterpolation.full is now promoted to Constants.java with @Config(type = "java.lang.Boolean", defaultValue = "false"), @since 4.1.0, and comprehensive Javadoc. The compat module's duplicate is correctly made private with a cross-reference comment explaining why it can't import from maven-api-core. Tests updated to use Constants.MAVEN_MODEL_DEPENDENCY_INTERPOLATION_FULL.

  2. Future-proofing note → Fixed. Clear design-intent comment added to mergeRepositories explaining that future non-build RequestTypes intentionally fall through to recessive merging.

Both observations from the previous review are resolved. No new issues in the delta.

This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

Claude Code on behalf of Guillaume Nodet

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants