-
Notifications
You must be signed in to change notification settings - Fork 146
Add fetch metadata module #2271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 5.4.x
Are you sure you want to change the base?
Changes from 7 commits
790e98c
0341e33
5773660
6174fbd
36fb47e
455d9c7
4ac826e
2c1af47
2d1abb2
d35f1b8
2c6ae28
2bf9734
6329a6b
807e42a
c1264ad
18b9bde
6072698
316a4d3
197caa9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import io.micronaut.build.TestFramework | ||
|
|
||
| plugins { | ||
| id("io.micronaut.build.internal.security-module") | ||
| } | ||
| dependencies { | ||
| api(mn.micronaut.http) | ||
| api(mnValidation.validation) | ||
| compileOnly(mn.micronaut.http.server) | ||
| testAnnotationProcessor(mn.micronaut.inject.java) | ||
| testImplementation(mnTest.micronaut.test.junit5) | ||
| testImplementation(mn.micronaut.http.client) | ||
| testImplementation(mn.micronaut.http.server.netty) | ||
| testImplementation(mnSerde.micronaut.serde.jackson) | ||
| testRuntimeOnly(mnTest.junit.jupiter.engine) | ||
| testRuntimeOnly(mnLogging.logback.classic) | ||
| } | ||
| tasks.withType<Test> { | ||
| useJUnitPlatform() | ||
| } | ||
| micronautBuild { | ||
| binaryCompatibility.enabledAfter("5.4.0") | ||
| testFramework = TestFramework.JUNIT6 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| /* | ||
| * Copyright 2017-2026 original authors | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package io.micronaut.security.fetchmetadata; | ||
|
|
||
| import io.micronaut.context.annotation.Requires; | ||
| import io.micronaut.core.annotation.Internal; | ||
| import io.micronaut.core.order.Ordered; | ||
| import io.micronaut.core.util.StringUtils; | ||
| import io.micronaut.http.HttpRequest; | ||
| import io.micronaut.http.HttpResponse; | ||
| import io.micronaut.http.HttpStatus; | ||
| import io.micronaut.http.MutableHttpResponse; | ||
| import io.micronaut.http.annotation.RequestFilter; | ||
| import io.micronaut.http.annotation.ServerFilter; | ||
| import io.micronaut.http.filter.ServerFilterPhase; | ||
| import org.jspecify.annotations.Nullable; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Server filter that enforces the configured Fetch Metadata request-isolation policy. | ||
| */ | ||
| @Requires(property = FetchMetadataFilterConfigurationProperties.PROPERTY_ENABLED, value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) | ||
| @Requires(classes = ServerFilter.class) | ||
| @ServerFilter("${" + FetchMetadataFilterConfigurationProperties.PREFIX + ".pattern:" + ServerFilter.MATCH_ALL_PATTERN + "}") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An empty This reads the raw property placeholder, and nothing in the module injects Set
Injecting the configuration bean and deriving the pattern from it would make the guard real. |
||
| @Internal | ||
| public final class FetchMetadataFilter implements Ordered { | ||
| private static final Logger LOG = LoggerFactory.getLogger(FetchMetadataFilter.class); | ||
| private final List<FetchMetadataRule<HttpRequest<?>>> rules; | ||
|
|
||
| /** | ||
| * @param rules Fetch Metadata rules, in evaluation order | ||
| */ | ||
| FetchMetadataFilter(List<FetchMetadataRule<HttpRequest<?>>> rules) { | ||
| this.rules = List.copyOf(rules); | ||
| } | ||
|
|
||
| /** | ||
| * Runs Fetch Metadata isolation before authentication and authorization filters. | ||
| * | ||
| * @return an order before the main security filter | ||
| */ | ||
| @Override | ||
| public int getOrder() { | ||
| return ServerFilterPhase.SECURITY.before(); | ||
| } | ||
|
|
||
| /** | ||
| * Evaluates the request against the configured Fetch Metadata rules. | ||
| * | ||
| * <p>The first rule that returns {@link FetchMetadataRuleResult#ALLOWED} or | ||
| * {@link FetchMetadataRuleResult#REJECTED} decides the result. The filter denies the request | ||
| * when every rule returns {@link FetchMetadataRuleResult#UNKNOWN}.</p> | ||
| * | ||
| * @param request the request to evaluate | ||
| * @return {@code null} to continue processing, or a forbidden response to reject the request | ||
| */ | ||
| @RequestFilter | ||
| @Nullable | ||
| @Internal | ||
| public HttpResponse<?> filterRequest(HttpRequest<?> request) { | ||
| for (FetchMetadataRule<HttpRequest<?>> rule : rules) { | ||
| FetchMetadataRuleResult result = rule.check(request); | ||
| if (result == FetchMetadataRuleResult.ALLOWED) { | ||
| if (LOG.isTraceEnabled()) { | ||
| LOG.trace("request {} {} approved by rule {}", request.getMethod(), request.getPath(), rule.getClass().getSimpleName()); | ||
| } | ||
| return null; // proceed | ||
|
|
||
| } else if (result == FetchMetadataRuleResult.REJECTED) { | ||
| if (LOG.isTraceEnabled()) { | ||
| LOG.trace("request {} {} rejected by rule {}", request.getMethod(), request.getPath(), rule.getClass().getSimpleName()); | ||
| } | ||
| return forbidden(); | ||
| } | ||
| } | ||
| if (LOG.isTraceEnabled()) { | ||
| LOG.trace("request {} {} rejected because no Fetch Metadata rule allowed it", | ||
| request.getMethod(), request.getPath()); | ||
| } | ||
| return forbidden(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A 403 from a security filter is usually worth more than Suggest |
||
| } | ||
|
|
||
| private static MutableHttpResponse<Object> forbidden() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The rejection is a bare Even just a |
||
| return HttpResponse.status(HttpStatus.FORBIDDEN); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| /* | ||
| * Copyright 2017-2026 original authors | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package io.micronaut.security.fetchmetadata; | ||
|
|
||
| import io.micronaut.core.util.Toggleable; | ||
|
|
||
| /** | ||
| * Configuration for the Fetch Metadata request filter. | ||
| * | ||
| * @since 5.4.0 | ||
| */ | ||
| public interface FetchMetadataFilterConfiguration extends Toggleable { | ||
| /** | ||
| * Returns the request path pattern to which the Fetch Metadata filter applies. | ||
| * | ||
| * @return the pattern the {@link FetchMetadataFilter} should match | ||
| * @since 5.4.0 | ||
| */ | ||
| String getPattern(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| /* | ||
| * Copyright 2017-2026 original authors | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package io.micronaut.security.fetchmetadata; | ||
|
|
||
| import io.micronaut.context.annotation.ConfigurationProperties; | ||
| import io.micronaut.core.annotation.Internal; | ||
| import io.micronaut.core.util.StringUtils; | ||
| import jakarta.validation.constraints.NotBlank; | ||
|
|
||
| /** | ||
| * Binds configuration for the Fetch Metadata request filter. | ||
| * | ||
| * @since 5.4.0 | ||
| */ | ||
| @ConfigurationProperties(FetchMetadataFilterConfigurationProperties.PREFIX) | ||
| @Internal | ||
| final class FetchMetadataFilterConfigurationProperties implements FetchMetadataFilterConfiguration { | ||
| /** Configuration prefix for the Fetch Metadata request filter. */ | ||
| public static final String PREFIX = "micronaut.security.fetch-metadata.filter"; | ||
| /** Property that enables or disables the Fetch Metadata request filter. */ | ||
| public static final String PROPERTY_ENABLED = FetchMetadataFilterConfigurationProperties.PREFIX + ".enabled"; | ||
| /** Default filter enablement. */ | ||
| public static final boolean DEFAULT_ENABLED = true; | ||
| /** Default server-filter pattern. */ | ||
| public static final String DEFAULT_PATTERN = "/**"; | ||
|
|
||
| @NotBlank | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Either wire up validation properly or drop the annotation and the Unrelated nit on line 30: this class is package-private while |
||
| private String pattern = DEFAULT_PATTERN; | ||
|
|
||
| private boolean enabled = DEFAULT_ENABLED; | ||
|
|
||
| /** | ||
| * Reports whether the Fetch Metadata request filter is enabled. | ||
| * | ||
| * @return whether the Fetch Metadata request filter is enabled | ||
| * @since 5.4.0 | ||
| */ | ||
| @Override | ||
| public boolean isEnabled() { | ||
| return this.enabled; | ||
| } | ||
|
|
||
| @Override | ||
| public String getPattern() { | ||
| return this.pattern; | ||
| } | ||
|
|
||
| /** | ||
| * Enables or disables the Fetch Metadata request filter. | ||
| * | ||
| * @param enabled whether the filter is enabled; defaults to {@value #DEFAULT_ENABLED} | ||
| * @since 5.4.0 | ||
| */ | ||
| public void setEnabled(boolean enabled) { | ||
| this.enabled = enabled; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the request path pattern matched by the Fetch Metadata request filter. | ||
| * Requests outside this pattern are not evaluated by this module. | ||
| * | ||
| * @param pattern the server-filter pattern; defaults to {@value #DEFAULT_PATTERN} | ||
| * @since 5.4.0 | ||
| */ | ||
| public void setPattern(String pattern) { | ||
| if (StringUtils.isNotEmpty(pattern)) { | ||
| this.pattern = pattern; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /* | ||
| * Copyright 2017-2026 original authors | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package io.micronaut.security.fetchmetadata; | ||
|
|
||
| import io.micronaut.core.order.Ordered; | ||
|
|
||
| /** | ||
| * Evaluates a request using Fetch Metadata or related request information. | ||
| * | ||
| * <p>Rules are evaluated in {@link Ordered order}. A rule should return | ||
| * {@link FetchMetadataRuleResult#UNKNOWN} when it does not apply so another rule can decide. | ||
| * Custom rules can override {@link #getOrder()} to select their position relative to the | ||
| * built-in policy rules.</p> | ||
| * | ||
| * @param <T> request type | ||
| * @since 5.4.0 | ||
| */ | ||
| public interface FetchMetadataRule<T> extends Ordered { | ||
| /** | ||
| * Evaluates a request. | ||
| * | ||
| * @param request the request to evaluate | ||
| * @return the rule result | ||
| */ | ||
| FetchMetadataRuleResult check(T request); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| /* | ||
| * Copyright 2017-2026 original authors | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package io.micronaut.security.fetchmetadata; | ||
|
|
||
| /** | ||
| * Result of evaluating a {@link FetchMetadataRule}. | ||
| * | ||
| * @since 5.4.0 | ||
| */ | ||
| public enum FetchMetadataRuleResult { | ||
| /** | ||
| * The rule explicitly allows this request. | ||
| */ | ||
| ALLOWED, | ||
|
|
||
| /** | ||
| * The rule explicitly rejects this request. | ||
| */ | ||
| REJECTED, | ||
|
|
||
| /** | ||
| * The rule has no information to make the determination. | ||
| */ | ||
| UNKNOWN | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| /* | ||
| * Copyright 2017-2026 original authors | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package io.micronaut.security.fetchmetadata; | ||
|
|
||
| import io.micronaut.http.HttpRequest; | ||
| import io.micronaut.http.SecFetch; | ||
| import org.jspecify.annotations.Nullable; | ||
|
|
||
| /** | ||
| * A Fetch Metadata rule for {@link HttpRequest} instances. | ||
| * | ||
| * @since 5.4.0 | ||
| */ | ||
| public interface HttpRequestFetchMetadataRule extends FetchMetadataRule<HttpRequest<?>> { | ||
| /** | ||
| * Extracts {@link SecFetch} from the request and evaluates it. | ||
| * | ||
| * @param request the request to evaluate | ||
| * @return the rule result | ||
| */ | ||
| @Override | ||
| default FetchMetadataRuleResult check(HttpRequest<?> request) { | ||
| return check(request, request.getSecFetch()); | ||
| } | ||
|
|
||
| /** | ||
| * Evaluates the request and its parsed Fetch Metadata headers. | ||
| * | ||
| * @param request the request to evaluate | ||
| * @param secFetch parsed Fetch Metadata, or {@code null} when the required headers are absent or invalid | ||
| * @return the rule result | ||
| */ | ||
| FetchMetadataRuleResult check(HttpRequest<?> request, @Nullable SecFetch secFetch); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment on
FetchMetadataFilterConfigurationProperties: thisapidependency exists solely for a@NotBlankthat never gets evaluated. If the annotation goes, this can go with it.