Skip to content

Conversation

@JungJaeLee-JJ
Copy link

Motivation

Currently, AthenzService only supports static resource strings configured at build time using the resource(String) method. This limitation makes it difficult to implement dynamic authorization patterns where the resource needs to be determined from the HTTP request itself (e.g., path parameters, headers, or request body).

This PR introduces the AthenzResourceProvider interface to enable dynamic resource resolution based on request attributes, allowing more flexible Athenz authorization patterns.

Modifications

Dynamic Resource Provider Support

  • Add AthenzResourceProvider functional interface for dynamic resource resolution
  • Implement built-in providers:
    • PathAthenzResourceProvider: Extract resource from request path
    • HeaderAthenzResourceProvider: Extract resource from HTTP header
    • JsonBodyFieldAthenzResourceProvider: Extract resource from JSON request body field
  • Update AthenzService to use AthenzResourceProvider instead of static resource string
  • Add resourceProvider() method to AthenzServiceBuilder
  • Update serve() method to handle async resource resolution with proper error handling
    • Returns INTERNAL_SERVER_ERROR when resource resolution fails
    • Records metric for failed resolutions

Documentation & Testing

  • Add comprehensive Javadoc examples demonstrating both static and dynamic resource usage
  • Add integration tests for all resource provider implementations
    • Test path-based resource extraction
    • Test header-based resource extraction
    • Test JSON body field resource extraction

Metric Stability Improvements

  • Add resourceTagValue parameter to resourceProvider() method to prevent metric cardinality explosion
    • When using dynamic resource providers, resource values can vary per request
    • resourceTagValue provides a stable identifier for metric tagging (e.g., "admin", "users")
  • Update resource() method to automatically set resourceTagValue to the resource name
  • Modify serviceAdded() to use resourceTagValue for metric tags instead of dynamic resource values

Result

After this PR is merged:

  • ✅ Users can implement flexible authorization patterns with dynamic resource resolution
  • ✅ Existing code using resource() method remains fully compatible (backward compatible)
  • ✅ Clear error handling is provided for resource resolution failures
  • ✅ Comprehensive documentation and test coverage is included
  • ✅ Metric cardinality remains stable when using dynamic resources

Breaking Changes

  • AthenzService constructor now requires AthenzResourceProvider instead of String resource (internal API only)
  • Public API remains fully backward compatible

Introduce AthenzResourceProvider interface to enable dynamic resource
resolution in AthenzService, replacing the static resource configuration.

Changes:
- Add AthenzResourceProvider interface for dynamic resource provisioning
- Implement PathAthenzResourceProvider to extract resource from request path
- Implement HeaderAthenzResourceProvider to extract resource from HTTP headers
- Implement JsonBodyFieldAthenzResourceProvider to extract resource from JSON request body
- Modify AthenzService to support resourceProvider() in builder pattern
- Update AthenzServiceBuilder to accept AthenzResourceProvider
- Add comprehensive integration tests for all resource provider implementations
- Add unit tests for resource provider interface and implementations
- Update AthenzDocker test infrastructure with additional policies

This enhancement allows users to configure Athenz authorization based on
dynamic request attributes instead of static resource strings, enabling
more flexible access control patterns.

Breaking Changes:
- None (backward compatible - static resource() method still supported)

Example usage:
```java
AthenzService.builder(ztsBaseClient)
    .resourceProvider(new PathAthenzResourceProvider())
    .action("obtain")
    .newDecorator();
```
# Conflicts:
#	athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java
#	athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java
…ality explosion

Add mandatory resourceTagValue parameter to AthenzServiceBuilder when using dynamic resource providers to ensure stable metric tags and prevent metric cardinality issues.

Changes:
- Add resourceTagValue field to AthenzServiceBuilder and AthenzService
- Update resourceProvider() method to require resourceTagValue parameter
- Update resource() method to automatically set resourceTagValue to the resource name
- Modify metric tag configuration to use resourceTagValue instead of dynamic resource values
- Add validation to ensure resourceTagValue is not empty
- Update serve() method to handle empty resource resolution with proper error response

Rationale:
When using dynamic resource providers (e.g., PathAthenzResourceProvider), the resource
value can vary per request, potentially creating unlimited unique metric combinations.
The resourceTagValue parameter provides a static identifier (e.g., "admin", "dynamic")
for metric tagging, ensuring stable cardinality while preserving metric usefulness.

Example:
- Static resource: resource("users") → resourceTagValue = "users"
- Dynamic resource: resourceProvider(new PathAthenzResourceProvider(), "admin")
  → resourceTagValue = "admin" (stable tag regardless of actual path)
@CLAassistant
Copy link

CLAassistant commented Dec 8, 2025

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link

coderabbitai bot commented Dec 8, 2025

Walkthrough

Replaces static Athenz resource configuration with an asynchronous AthenzResourceProvider model. Resources can be derived from request path, headers, or JSON body before access checks; resolution failures map to 403/500, and a resourceTagValue propagates the resolved resource into metrics.

Changes

Cohort / File(s) Summary
Core service & builder
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java, athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java
Switched to provider-based resource resolution: AthenzService now resolves resources asynchronously via AthenzResourceProvider and maps null/empty to 403 (AthenzResourceNotFoundException) and provider exceptions to 500. Added resourceTagValue for metrics; builder gains resourceProvider(...) APIs and preserves static-resource behavior via a provider wrapper.
Provider interface
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProvider.java
New public functional interface with CompletableFuture<String> provide(ServiceRequestContext, HttpRequest) and static factories: ofPath(), ofPath(boolean), ofHeader(String), ofJsonField(JsonPointer), ofJsonField(String).
Provider implementations
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java, athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java, athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java
Added package-private providers extracting resource from path (with/without query), a header, or a JSON body field. Missing/invalid inputs produce AthenzResourceNotFoundException; JSON provider parses body and uses JsonPointer.
Error type
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceNotFoundException.java
New public unchecked exception indicating failure to extract a non-null, non-empty Athenz resource (constructors for message and cause).
Package metadata
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/package-info.java
New package-level Javadoc and annotations: @UnstableApi and @NonNullByDefault.
Tests — infra & constants
athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzDocker.java
Added test policy constants ADMIN_PATH_POLICY, ADMIN_HEADER_POLICY, ADMIN_JSON_POLICY and corresponding policies to test path/header/json providers.
Integration tests
athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java
New integration tests exercising path/header/json providers, provider exceptions, null/empty results, and parameterized TokenType flows for allow/deny outcomes.
Unit tests
athenz/src/test/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProviderTest.java
New unit tests covering providers: path with/without query, header present/missing/empty, JSON field extraction and failures, and CompletableFuture behaviors.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant AthenzService
    participant AthenzResourceProvider
    participant AccessControl
    participant WrappedService

    Client->>AthenzService: HTTP Request
    AthenzService->>AthenzResourceProvider: provide(ctx, req)

    alt Resource resolved (non-empty)
        AthenzResourceProvider-->>AthenzService: CompletableFuture<String> (resource)
        AthenzService->>AccessControl: check(action, resource, token)
        alt Access allowed
            AccessControl-->>AthenzService: allow
            AthenzService->>WrappedService: delegate(request)
            WrappedService-->>Client: 200 OK
        else Access denied
            AccessControl-->>AthenzService: deny
            AthenzService-->>Client: 401 Unauthorized
        end
    else Resource missing/empty
        AthenzResourceProvider-->>AthenzService: AthenzResourceNotFoundException
        AthenzService-->>Client: 403 Forbidden
    else Provider exception
        AthenzResourceProvider-->>AthenzService: Exception
        AthenzService-->>Client: 500 Internal Server Error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay special attention to:
    • Async control flow and exception-to-HTTP-status mapping in AthenzService.java.
    • Backward-compatibility and validation logic in AthenzServiceBuilder.java.
    • JSON parsing and JsonPointer handling in JsonBodyFieldAthenzResourceProvider.java.
    • Test coverage in AthenzResourceProviderIntegrationTest.java for edge cases (null/empty provider results and provider exceptions).

Possibly related PRs

Suggested reviewers

  • trustin
  • minwoox
  • jrhee17

Poem

🐰 I nibble at headers, paths, and JSON stems,
Async carrots, one provider per gem,
Resolved and tagged, I hop through the gate,
Exceptions or empties—403 or fate,
Metrics in paw, I celebrate with a drum. 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.73% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering motivation, detailed modifications, and results with clear explanations of both dynamic and backward-compatible aspects.
Title check ✅ Passed The pull request title clearly and specifically describes the main feature being added: dynamic Athenz resource resolution via a new AthenzResourceProvider interface, which aligns with the comprehensive changeset across multiple classes and test files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)

106-110: Bug: tokenType(TokenType...) method doesn't store the value.

This method validates the input but never assigns to this.tokenTypes, so the varargs overload is non-functional. The Iterable overload at lines 116-121 works correctly.

 public AthenzServiceBuilder tokenType(TokenType... tokenTypes) {
     requireNonNull(tokenTypes, "tokenTypes");
     checkArgument(tokenTypes.length > 0, "tokenTypes must not be empty");
+    this.tokenTypes = ImmutableList.copyOf(tokenTypes);
     return this;
 }
🧹 Nitpick comments (7)
athenz/src/test/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProviderTest.java (2)

31-31: Typo in method names: "Exits" should be "Exists".

Multiple test method names contain "Exits" instead of "Exists":

  • shouldProvideHeaderStringIfExitsshouldProvideHeaderStringIfExists
  • shouldProvideEmptyStringIfHeaderNotExitsshouldProvideEmptyStringIfHeaderNotExists
  • shouldProvideJsonFieldStringIfExitsshouldProvideJsonFieldStringIfExists
  • shouldProvideEmptyStringIfJsonFieldNotExitsshouldProvideEmptyStringIfJsonFieldNotExists

Also applies to: 45-45, 59-59, 75-75


74-87: Misleading test name and description.

The test shouldProvideEmptyStringIfJsonFieldNotExits actually tests invalid JSON parsing ({invalid json}), not a missing field scenario. Consider either:

  1. Renaming to shouldProvideEmptyStringIfJsonIsInvalid, or
  2. Adding a separate test for the actual missing field case with valid JSON like {"otherField": "value"}
 @Test
-public void shouldProvideEmptyStringIfJsonFieldNotExits() {
+public void shouldProvideEmptyStringIfJsonIsInvalid() {
     // given
     final ObjectMapper mapper = new ObjectMapper();
     final JsonBodyFieldAthenzResourceProvider provider = new JsonBodyFieldAthenzResourceProvider(mapper, "resourceId");
     final HttpRequest req = HttpRequest.of(RequestHeaders.of(HttpMethod.POST, "/", "Content-Type", "application/json"), HttpData.ofUtf8("{invalid json}"));
     final ServiceRequestContext ctx = ServiceRequestContext.of(req);

     // when
     final CompletableFuture<String> result = provider.provide(ctx, req);

     // then
     assertThat(result.join()).isEmpty();
 }
+
+@Test
+public void shouldProvideEmptyStringIfJsonFieldNotExists() {
+    // given
+    final ObjectMapper mapper = new ObjectMapper();
+    final JsonBodyFieldAthenzResourceProvider provider = new JsonBodyFieldAthenzResourceProvider(mapper, "resourceId");
+    final HttpRequest req = HttpRequest.of(RequestHeaders.of(HttpMethod.POST, "/", "Content-Type", "application/json"), HttpData.ofUtf8("{\"otherField\":\"value\"}"));
+    final ServiceRequestContext ctx = ServiceRequestContext.of(req);
+
+    // when
+    final CompletableFuture<String> result = provider.provide(ctx, req);
+
+    // then
+    assertThat(result.join()).isEmpty();
+}
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProvider.java (1)

26-31: Consider adding Javadoc for the public interface and method.

The interface lacks documentation. Adding Javadoc would improve API usability and align with the other provider implementations that have comprehensive documentation.

+/**
+ * Provides an Athenz resource string dynamically based on the request context.
+ *
+ * <p>Implementations extract the resource identifier from various sources such as
+ * the request path, headers, or body to be used for Athenz authorization checks.
+ */
 @UnstableApi
 @FunctionalInterface
 public interface AthenzResourceProvider {
 
+    /**
+     * Resolves the Athenz resource string for the given request.
+     *
+     * @param ctx the service request context
+     * @param req the HTTP request
+     * @return a {@link CompletableFuture} containing the resolved resource string,
+     *         or an empty string if resolution fails
+     */
     CompletableFuture<String> provide(ServiceRequestContext ctx, HttpRequest req);
 }
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1)

50-52: Add null/empty validation for headerName parameter.

The constructor should validate the input to fail fast with a clear error message, consistent with the validation pattern used in AthenzServiceBuilder.

+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
     public HeaderAthenzResourceProvider(String headerName) {
+        requireNonNull(headerName, "headerName");
+        checkArgument(!headerName.isEmpty(), "headerName must not be empty");
         this.headerName = headerName;
     }
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (2)

66-69: Add null/empty validation for constructor parameters.

The constructor should validate inputs to fail fast with clear error messages, consistent with the validation pattern used elsewhere in this PR.

+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
     public JsonBodyFieldAthenzResourceProvider(ObjectMapper objectMapper, String jsonFieldName) {
+        requireNonNull(objectMapper, "objectMapper");
+        requireNonNull(jsonFieldName, "jsonFieldName");
+        checkArgument(!jsonFieldName.isEmpty(), "jsonFieldName must not be empty");
         this.objectMapper = objectMapper;
         this.jsonFieldName = jsonFieldName;
     }

85-86: Consider logging the exception for debugging purposes.

Silently swallowing exceptions with catch (Exception ignored) can make debugging difficult in production. Consider logging at DEBUG or TRACE level to aid troubleshooting while still returning an empty string.

+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class JsonBodyFieldAthenzResourceProvider implements AthenzResourceProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(JsonBodyFieldAthenzResourceProvider.class);
+
     ...
 
-        } catch (Exception ignored) {
+        } catch (Exception e) {
+            logger.debug("Failed to extract JSON field '{}' from request body", jsonFieldName, e);
             return "";
         }
athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java (1)

153-172: Consider adding a test for missing header scenario.

The test validates an invalid header value, but it would be valuable to also test the case where the X-Athenz-Resource header is completely absent. Based on HeaderAthenzResourceProvider, this would return an empty string and should result in INTERNAL_SERVER_ERROR.

@ParameterizedTest
@EnumSource(TokenType.class)
void shouldReturnInternalServerErrorWhenHeaderIsMissing(TokenType tokenType) {
    try (ZtsBaseClient ztsBaseClient = athenzExtension.newZtsBaseClient(TEST_SERVICE)) {
        final BlockingWebClient client =
                WebClient.builder(server.httpUri())
                        .decorator(AthenzClient.newDecorator(ztsBaseClient, TEST_DOMAIN_NAME,
                                ADMIN_ROLE, tokenType))
                        .build()
                        .blocking();

        // Request without X-Athenz-Resource header
        final AggregatedHttpResponse response = client.get("/admin/header/test");
        assertThat(response.status()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 770904e and 9dd1ec9.

📒 Files selected for processing (9)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (4 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (4 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProvider.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (1 hunks)
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzDocker.java (2 hunks)
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java (1 hunks)
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProviderTest.java (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzDocker.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProvider.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProviderTest.java
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java
🧬 Code graph analysis (3)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProvider.java (2)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (1)
  • UnstableApi (106-220)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (1)
  • UnstableApi (43-50)
athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java (3)
athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzDocker.java (1)
  • AthenzDocker (55-342)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1)
  • HeaderAthenzResourceProvider (40-59)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1)
  • JsonBodyFieldAthenzResourceProvider (54-89)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (1)
core/src/main/java/com/linecorp/armeria/common/util/Exceptions.java (1)
  • Exceptions (55-426)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (10)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (1)

43-50: LGTM!

Clean implementation with proper @UnstableApi annotation and comprehensive Javadoc. The class is appropriately marked final for a simple, non-extensible provider.

athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzDocker.java (1)

75-79: LGTM!

The new policy constants and their creation in defaultScaffold() correctly set up test fixtures for path, header, and JSON body-based resource providers. Minor: there's an extra blank line at line 171.

Also applies to: 166-171

athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (2)

66-87: LGTM on the resource provider implementation.

The resource() and resourceProvider() methods are well-implemented with proper validation. The design correctly handles both static resources (via resource()) and dynamic resolution (via resourceProvider()), with resourceTagValue used for stable metric tagging to avoid cardinality explosion.


126-138: Approve the updated newDecorator() implementation.

The state checks for athenzResourceProvider and resourceTagValue are appropriate, and the constructor call correctly passes all required parameters.

athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (3)

48-105: Well-documented API with clear examples.

The Javadoc provides clear examples for both static and dynamic resource configurations, which helps users understand the intended usage patterns. The @see references to the concrete provider implementations are helpful.


129-153: Good backward-compatible constructor design.

The dual-constructor approach maintains API compatibility while enabling the new provider-based resolution. The static resource constructor at line 142-153 correctly wraps the resource string in a CompletableFuture, and using resourceTagValue for metrics prevents cardinality explosion from dynamic resources.


155-168: LGTM!

The metric initialization correctly uses resourceTagValue for stable tagging, preventing metric cardinality issues when dynamic resources are used.

athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java (3)

55-94: Well-structured test setup with proper resource management.

The @Order annotations ensure athenzExtension initializes before the server. The ServerListener properly closes the ztsBaseClient on server shutdown, preventing resource leaks.


96-128: LGTM!

The path resource tests properly cover both allowed and denied scenarios with appropriate role configurations. The try-with-resources pattern ensures proper cleanup of ZtsBaseClient.


174-219: Comprehensive JSON body resource tests.

The tests properly verify JSON body field extraction for both valid and invalid resource values. The content type is correctly set to MediaType.JSON.

Consider adding edge case tests for malformed JSON or missing resource field if thorough coverage is desired, as JsonBodyFieldAthenzResourceProvider returns empty string in these cases.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (2)

87-89: Consider logging or documenting the silent exception handling.

Swallowing all exceptions silently can make debugging difficult when JSON parsing fails unexpectedly. While returning an empty string is acceptable behavior (leading to INTERNAL_SERVER_ERROR), consider adding a debug-level log or at minimum documenting this behavior in the Javadoc.

+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class JsonBodyFieldAthenzResourceProvider implements AthenzResourceProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(JsonBodyFieldAthenzResourceProvider.class);
+
     ...
 
         } catch (Exception ignored) {
+            logger.debug("Failed to parse JSON body for field '{}': {}", jsonFieldName, ignored.getMessage());
             return "";
         }

55-56: Consider making the class final for consistency.

PathAthenzResourceProvider is declared final while this class and HeaderAthenzResourceProvider are not. For consistency across resource providers and to prevent unintended subclassing, consider making this class final.

 @UnstableApi
-public class JsonBodyFieldAthenzResourceProvider implements AthenzResourceProvider {
+public final class JsonBodyFieldAthenzResourceProvider implements AthenzResourceProvider {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9dd1ec9 and 3bcce46.

📒 Files selected for processing (3)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (4 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java
🧬 Code graph analysis (1)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (4)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (1)
  • UnstableApi (106-222)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1)
  • UnstableApi (41-61)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (1)
  • UnstableApi (43-50)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)
  • UnstableApi (41-139)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (3)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (3)

142-153: LGTM on backward-compatible constructor.

This package-private constructor maintains backward compatibility by wrapping the static athenzResource string in a CompletableFuture and using it as the resourceTagValue. Clean approach for supporting legacy callers.


190-206: Appropriate use of blockingTaskExecutor for ZPE access checks.

The authZpeClient.allowAccess() call is correctly offloaded to ctx.blockingTaskExecutor() since ZPE policy evaluation may involve blocking I/O. The error handling within the async block properly propagates exceptions via Exceptions.throwUnsafely(e).


160-167: LGTM on metric tagging with resourceTagValue.

Using resourceTagValue instead of the dynamically resolved resource for metric tags is the correct approach to avoid metric cardinality explosion. This aligns with the PR objectives.

- Improve AthenzService validation
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (1)

181-188: Consider logging resource resolution failures for debugging.

When athenzResourceProvider.provide() fails, the exception is silently converted to an empty string (line 182), and the user receives a generic "Failed to resolve resource" message. While the error response and timer recording are correct, the root cause is not logged, making it difficult to diagnose why resource resolution failed (e.g., malformed JSON, missing header, network timeout).

Consider logging the exception:

-        final CompletableFuture<HttpResponse> future = athenzResourceProvider.provide(ctx, req)
-                .exceptionally(cause -> "")
+        final CompletableFuture<HttpResponse> future = athenzResourceProvider.provide(ctx, req)
+                .exceptionally(cause -> {
+                    logger.warn("Failed to resolve Athenz resource", cause);
+                    return "";
+                })
                 .thenCompose(athenzResource -> {

Note: You'll need to add a logger field if not already present.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bcce46 and 80e7622.

📒 Files selected for processing (4)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (4 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java
🧬 Code graph analysis (1)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (4)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (1)
  • UnstableApi (106-222)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1)
  • UnstableApi (45-67)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1)
  • UnstableApi (58-97)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)
  • UnstableApi (41-139)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (3)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (1)

26-49: Path-based resource provider implementation and API stability annotation look good

Javadoc, naming, and behavior are consistent: the provider returns the request path via a cheap completedFuture, matching how other AthenzResourceProvider implementations work. The class is correctly annotated with @UnstableApi, so the new public surface complies with the API stability guidelines. No changes needed.

athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (2)

51-105: Excellent documentation with practical examples.

The comprehensive Javadoc clearly distinguishes static and dynamic resource configurations with practical code examples. This will help users understand when and how to use each approach.


160-167: Good use of resourceTagValue for metric stability.

Using resourceTagValue instead of the dynamic resource value prevents metric cardinality explosion, which is important when resources vary per request.

@ikhoon ikhoon added this to the 1.35.0 milestone Dec 8, 2025
Copy link
Contributor

@jrhee17 jrhee17 left a comment

Choose a reason for hiding this comment

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

Left some questions, but overall looks good

final JsonNode node = root.get(jsonFieldName);
return node != null ? node.asText("") : "";
} catch (Exception ignored) {
return "";
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional) Rather than the overall pattern of swallowing the exception, it may be worth propagating the exception so that it could possibly be logged with a debug level log. Otherwise, it may be difficult to debug why a certain ResourceProvider is failing

Copy link
Author

@JungJaeLee-JJ JungJaeLee-JJ Dec 9, 2025

Choose a reason for hiding this comment

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

I’ve added debug logging for both JsonBodyFieldAthenzResourceProvider and HeaderAthenzResourceProvider in the following commit: 5306310

@FunctionalInterface
public interface AthenzResourceProvider {

CompletableFuture<String> provide(ServiceRequestContext ctx, HttpRequest req);
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor Question) It seems like the overall pattern is providing resources. Is there no need to also determine the action dynamically?

Copy link
Author

Choose a reason for hiding this comment

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

From my experience using Athenz, actions are often mapped to HTTP methods (GET, PUT, POST, DELETE, etc.), while resources correspond to paths. In many cases, we simply allow all actions using * and perform authorization primarily based on resources. For that reason, this PR adds only the resource for now.

If we later find a need to handle actions more granularly, we can introduce an action provider at that time.

* @param objectMapper the {@link ObjectMapper} used to parse the JSON request body
* @param jsonFieldName the name of the JSON field to extract the resource from
*/
public JsonBodyFieldAthenzResourceProvider(ObjectMapper objectMapper, String jsonFieldName) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit; it may be worth using json path instead of assuming 1-level.

Copy link
Author

Choose a reason for hiding this comment

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

The assumption about using a 1-level JSON structure comes from how Athenz typically maps resources to paths.
The JsonBodyFieldAthenzResourceProvider was introduced specifically to support platforms that already operate non-RESTful APIs.

From a general authentication/authorization perspective, tenant-specific resource values are usually provided at the first level of the JSON body, which covers the majority of use cases.
For example, when identifying a specific service with a field like service_id, the payload commonly looks like:

{
  "service_id": "service_id",
  ...
}

We don’t often see cases where such identifiers appear at level 2 or deeper in the JSON structure, so the current assumption seems sufficient for now.

Copy link
Contributor

@ikhoon ikhoon Dec 9, 2025

Choose a reason for hiding this comment

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

In most cases, only first-level fields are expected to be accessed. However, since Armeria is a framework, we need to account for a variety of scenarios. It would be nice to support both a simple case—accepting a String to access a first-level field—and a more general implementation that takes Jackson's JsonPointer.
I suggested JsonPointer since Jackson is part of our public API.

interface AthenzResourceProvider {

  static AthenzResourceProvider ofJsonField(String jsonFieldName) {
    ...
  }

  static AthenzResourceProvider ofJsonField(JsonPointer jsonPointer) {
    ...
  }
}

Copy link
Author

Choose a reason for hiding this comment

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

I've implemented both simple field name and JsonPointer support as you suggested.

Changes in 1aad9a1:

  • Made implementation constructors protected to encapsulate instantiation logic
  • Added factory methods in AthenzResourceProvider interface:
    • ofPath() - for path-based resource extraction
    • ofHeader(String headerName) - for header-based extraction
    • ofJsonField(String jsonFieldName) - for simple first-level JSON fields
    • ofJsonField(JsonPointer jsonPointer) - for nested JSON fields using Jackson's JsonPointer

Now users can choose between simple and advanced usage:

// Simple first-level field
AthenzResourceProvider.ofJsonField("resourceId")

// Nested field with JsonPointer
AthenzResourceProvider.ofJsonField(JsonPointer.compile("/user/id"))


@Override
public CompletableFuture<String> provide(ServiceRequestContext ctx, HttpRequest req) {
return CompletableFuture.completedFuture(req.path());
Copy link
Contributor

Choose a reason for hiding this comment

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

Note) I believe req.path() will include query params, etc.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, that’s correct. In cases where tenant-specific authorization needs to consider query parameters, the provider also includes the query params when returning the resource value. This ensures that scenarios restricted by query parameters are also properly supported.

Copy link
Contributor

Choose a reason for hiding this comment

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

  • Should we add an option to determine whether to include query parameters for Athenz resources?
  • Armeria typically hides implementation details and provides factory methods through the interface instead. For example:
public interface AthenzResourceProvider {

  static AthenzResourceProvider ofPath() {
    return PathAthenzResourceProvider.INSTANCE;
  }

  static AthenzResourceProvider ofPath(boolean includeQuery) {
    if (includeQuery) {
      return PathAthenzResourceProvider.QUERY_INSTANCE;
    } else {
      return PathAthenzResourceProvider.INSTANCE;
    }
  }
}

Copy link
Author

Choose a reason for hiding this comment

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

I've implemented query parameter support and exposed factory methods through the interface as you proposed.

Changes in 1aad9a1


@Override
public CompletableFuture<String> provide(ServiceRequestContext ctx, HttpRequest req) {
return req.aggregate().thenApply(this::extractFieldSafely);
Copy link
Contributor

Choose a reason for hiding this comment

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

I missed this during the initial review, but it probably makes more sense to aggregate only if the request has content-type: json

Copy link
Author

Choose a reason for hiding this comment

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

I’ve updated the implementation in 5306310
so that aggregation is performed only when the request’s Content-Type is JSON.

…Provider when exception occurred or resource not found
Comment on lines 182 to 187
.exceptionally(cause -> "")
.thenCompose(athenzResource -> {
if (athenzResource == null || athenzResource.isEmpty()) {
assert deniedTimer != null;
deniedTimer.record(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
return CompletableFuture.completedFuture(HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, MediaType.PLAIN_TEXT, "Failed to resolve resource"));
Copy link
Contributor

Choose a reason for hiding this comment

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

We should distinguish between exceptions and a null resources. I suggest returning INTERNAL_SERVER_ERROR for exceptions and FORBIDDEN when a resource cannot be resolved.

Copy link
Author

Choose a reason for hiding this comment

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

I've updated the implementation to distinguish between exceptions and null resources.

Changes in 1aad9a1:

  • Return INTERNAL_SERVER_ERROR (500) when an exception occurs while resolving the resource
  • Return FORBIDDEN (403) when the resource cannot be resolved (null or empty)
  • Added test cases in AthenzResourceProviderIntegrationTest:
    • Exception handling during resource resolution → INTERNAL_SERVER_ERROR
    • Null resource handling → FORBIDDEN
    • Empty resource handling → FORBIDDEN

This provides clearer error responses for different failure scenarios.

"resource", athenzResource,
"action", athenzAction));
meterIdPrefix.tags("result", "denied",
"resource", resourceTagValue,
Copy link
Contributor

Choose a reason for hiding this comment

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

How about just setting * to the resource tag if athenzResourceProvider is specified?

Copy link
Author

@JungJaeLee-JJ JungJaeLee-JJ Dec 10, 2025

Choose a reason for hiding this comment

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

I've simplified the API by making * the default resource tag value when using AthenzResourceProvider.

Changes in 1aad9a1:

  • Made * the default resource tag value for resourceProvider(AthenzResourceProvider)
  • Kept resourceProvider(AthenzResourceProvider, String) overload for custom resource tag value use cases

Usage examples:

// Default: resource tag = "*"
AthenzService.builder(ztsBaseClient)
             .resourceProvider(AthenzResourceProvider.ofPath())
             .action("read")
             .newDecorator();

// Custom resource tag
AthenzService.builder(ztsBaseClient)
             .resourceProvider(AthenzResourceProvider.ofPath(), "custom-tag")
             .action("read")
             .newDecorator();

*
* <p><strong>Mandatory:</strong> Either this or {@link #resource(String)} must be set before calling {@link #newDecorator()}.</p>
*/
public AthenzServiceBuilder resourceProvider(AthenzResourceProvider athenzResourceProvider, String resourceTagValue) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we make resource() and resourceProvider() mutually exclusive?
For example:

checkState(athenzResource == null, 
           "resource() and resourceProvider() are mutually exclusive");

Copy link
Author

Choose a reason for hiding this comment

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

I've added validation to make resource() and resourceProvider() mutually exclusive.

/**
* Sets the {@link AthenzResourceProvider} used to resolve the Athenz resource dynamically for each request.
*
* <p><strong>Mandatory:</strong> Either this or {@link #resource(String)} must be set before calling {@link #newDecorator()}.</p>
Copy link
Contributor

Choose a reason for hiding this comment

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

style) This line exceeds our max line length.

<module name="LineLength">
<property name="max" value="112"/>

Copy link
Author

Choose a reason for hiding this comment

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

I've applied the LY OSS code style to match Armeria's formatting convention


@Override
public CompletableFuture<String> provide(ServiceRequestContext ctx, HttpRequest req) {
return CompletableFuture.completedFuture(req.path());
Copy link
Contributor

Choose a reason for hiding this comment

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

  • Should we add an option to determine whether to include query parameters for Athenz resources?
  • Armeria typically hides implementation details and provides factory methods through the interface instead. For example:
public interface AthenzResourceProvider {

  static AthenzResourceProvider ofPath() {
    return PathAthenzResourceProvider.INSTANCE;
  }

  static AthenzResourceProvider ofPath(boolean includeQuery) {
    if (includeQuery) {
      return PathAthenzResourceProvider.QUERY_INSTANCE;
    } else {
      return PathAthenzResourceProvider.INSTANCE;
    }
  }
}

* @param objectMapper the {@link ObjectMapper} used to parse the JSON request body
* @param jsonFieldName the name of the JSON field to extract the resource from
*/
public JsonBodyFieldAthenzResourceProvider(ObjectMapper objectMapper, String jsonFieldName) {
Copy link
Contributor

@ikhoon ikhoon Dec 9, 2025

Choose a reason for hiding this comment

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

In most cases, only first-level fields are expected to be accessed. However, since Armeria is a framework, we need to account for a variety of scenarios. It would be nice to support both a simple case—accepting a String to access a first-level field—and a more general implementation that takes Jackson's JsonPointer.
I suggested JsonPointer since Jackson is part of our public API.

interface AthenzResourceProvider {

  static AthenzResourceProvider ofJsonField(String jsonFieldName) {
    ...
  }

  static AthenzResourceProvider ofJsonField(JsonPointer jsonPointer) {
    ...
  }
}

Add AthenzResourceProvider interface to enable dynamic Athenz resource resolution from HTTP requests.

Changes:
- Add AthenzResourceProvider interface for dynamic resource extraction
- Add factory methods: ofPath(), ofHeader(), ofJsonField()
- Implement PathAthenzResourceProvider for path-based extraction
- Implement HeaderAthenzResourceProvider for header-based extraction
- Implement JsonBodyFieldAthenzResourceProvider for JSON body extraction
- Update AthenzService to support resourceProvider() in builder
- Add error handling:
  - 403 FORBIDDEN for null/empty resources
  - 500 INTERNAL_SERVER_ERROR for resolution exceptions
- Add integration tests for all provider types
- Update Javadoc with dynamic resource examples

The builder now supports both static and dynamic resources:
- Static: .resource("admin")
- Dynamic: .resourceProvider(AthenzResourceProvider.ofPath())
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)

116-120: tokenType(TokenType...) currently ignores its argument

Unrelated to this PR but visible here: the varargs tokenType(TokenType... tokenTypes) method only validates that tokenTypes.length > 0 and then returns this without updating the tokenTypes field, whereas the Iterable<TokenType> overload correctly sets this.tokenTypes = ImmutableList.copyOf(tokenTypes).

As a result, calling the varargs overload has no effect. It would be better to either:

  • Assign the field in this overload as well, or
  • Delegate to the iterable overload for consistency.
🧹 Nitpick comments (3)
athenz/src/test/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProviderTest.java (1)

47-74: Clarify test names and JSON “not exists” scenario

Test names have a small typo (ExitsExists), e.g. shouldProvideHeaderStringIfExits and shouldProvideEmptyStringIfHeaderNotExits. Also, shouldProvideEmptyStringIfJsonFieldNotExits currently exercises the “invalid JSON” path rather than a valid JSON body where the field is actually missing. Consider either:

  • Renaming the method to reflect the invalid-JSON case, or
  • Adding a separate test for “JSON field missing but body valid” to cover that branch explicitly.

Also applies to: 112-125

athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1)

32-48: Align Javadoc with logging behavior for empty header values

The Javadoc states that when the header is “not present or empty”, an empty string is returned and logged at debug level, but the implementation only logs when value == null (header missing), not when the header is present with an empty value.

Consider either:

  • Logging for both null and empty values, or
  • Updating the Javadoc to clarify that only the “not present” case is logged.

Also applies to: 67-75

athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (1)

179-218: Avoid exposing provider exception messages in 500 responses

The new error handling cleanly distinguishes:

  • Provider exceptions → 500
  • Unresolved/empty resource → 403
  • AuthZ deny/missing token → 401

However, the 500 branch currently returns:

HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, MediaType.PLAIN_TEXT,
                "Exception occurred while resolving resource: " + cause.getMessage())

This can leak internal exception details to clients (and whatever text the provider chose as its message). Consider instead:

  • Logging the exception at debug/warn (optionally using Exceptions.peel(cause)), and
  • Returning a generic body such as "Failed to resolve resource" or similar, without including cause.getMessage().

This keeps external behavior stable (still 500) while tightening error information exposure.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5306310 and 1aad9a1.

📒 Files selected for processing (8)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (4 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (4 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProvider.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (1 hunks)
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java (1 hunks)
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProviderTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProvider.java
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProviderTest.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java
🧬 Code graph analysis (4)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (4)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (1)
  • UnstableApi (108-232)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)
  • UnstableApi (41-149)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1)
  • UnstableApi (49-76)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1)
  • UnstableApi (77-130)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (3)
core/src/main/java/com/linecorp/armeria/common/metric/MoreMeters.java (1)
  • MoreMeters (42-203)
scala/scala_2.13/src/main/scala/com/linecorp/armeria/client/scala/ScalaRestClient.scala (1)
  • unwrap (187-187)
core/src/main/java/com/linecorp/armeria/common/util/Exceptions.java (1)
  • Exceptions (55-426)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProvider.java (5)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (1)
  • UnstableApi (108-232)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)
  • UnstableApi (41-149)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1)
  • UnstableApi (49-76)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1)
  • UnstableApi (77-130)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (1)
  • UnstableApi (55-72)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (4)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (1)
  • UnstableApi (108-232)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)
  • UnstableApi (41-149)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1)
  • UnstableApi (77-130)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (1)
  • UnstableApi (55-72)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (5)
athenz/src/test/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProviderTest.java (1)

19-45: Good coverage of built‑in providers

The tests exercise path (with and without query), header, and JSON field (name and JsonPointer) providers end‑to‑end using ServiceRequestContext and HttpRequest, which should give solid confidence in the new factories’ behavior.

Also applies to: 47-109

athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (1)

55-71: Path-based provider implementation looks correct

Using ctx.path() for the no‑query variant and req.path() for the query‑inclusive variant matches Armeria’s path semantics and the documented examples; the static instances avoid per‑request allocations.

athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProvider.java (1)

61-89: AthenzResourceProvider API and factory design look solid

The functional interface plus static factories (ofPath, ofHeader, ofJsonField) provide a clean, discoverable API while keeping implementation classes internal to the package. Marking the interface as @UnstableApi is consistent with the new surface area.

athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (1)

119-151: Constructor overloading and metric tag changes preserve behavior cleanly

Wrapping the legacy string resource in a lambda-based AthenzResourceProvider while introducing the new provider‑based constructor keeps existing call sites working and centralizes the resource resolution model. Using a separate resourceTagValue for metrics is a good way to avoid high cardinality when resources are dynamic.

Also applies to: 153-166

athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)

48-58: Builder wiring to provider-based AthenzService looks correct

Using DEFAULT_RESOURCE_TAG_VALUE = "*" when only a provider is supplied, and passing both athenzResourceProvider and resourceTagValue into the new AthenzService constructor ensures:

  • Dynamic resources don’t explode metric cardinality by default.
  • Static resources still end up with meaningful resource tags.

The overall transition from a static resource string to a provider + tag value is cleanly encapsulated in the builder.

Also applies to: 136-148

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)

117-121: tokenType(TokenType...) currently ignores its argument

Unrelated to this PR’s main change but surfaced while reviewing: the varargs tokenType(TokenType... tokenTypes) method validates the input but never assigns it to this.tokenTypes, so calls like builder.tokenType(TokenType.ACCESS_TOKEN) have no effect (the default list is still used in newDecorator()).

Suggest aligning it with the iterable overload:

    public AthenzServiceBuilder tokenType(TokenType... tokenTypes) {
        requireNonNull(tokenTypes, "tokenTypes");
        checkArgument(tokenTypes.length > 0, "tokenTypes must not be empty");
-       return this;
+       this.tokenTypes = ImmutableList.copyOf(tokenTypes);
+       return this;
    }

You can fix this here or in a small follow‑up PR, but it’s a real configuration bug.

🧹 Nitpick comments (2)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (2)

63-75: Consider delegating resource(String) to resourceProvider(...) to avoid duplication

The static-resource path is implemented correctly: null/empty checks, mutual exclusivity, and wrapping in a CompletableFuture look good. To keep the invariants in one place and reduce duplication, you could delegate to the 2‑arg overload:

    public AthenzServiceBuilder resource(String athenzResource) {
        requireNonNull(athenzResource, "athenzResource");
        checkArgument(!athenzResource.isEmpty(), "athenzResource must not be empty");
-       checkState(this.athenzResourceProvider == null, "resource() and resourceProvider() are mutually exclusive");
-       this.athenzResourceProvider = (ctx, req) -> CompletableFuture.completedFuture(athenzResource);
-       this.resourceTagValue = athenzResource;
-       return this;
+       return resourceProvider((ctx, req) -> CompletableFuture.completedFuture(athenzResource),
+                               athenzResource);
    }

This keeps the mutual‑exclusivity and resourceTagValue logic centralized.


77-98: resourceProvider overloads look good; small doc and naming nits

The overload pair correctly enforces non‑null, non‑empty resourceTagValue and mutual exclusivity with resource(), and the default "*" tag is a sensible choice for bounded metric cardinality. Two minor polish suggestions:

  • In requireNonNull(athenzResourceProvider, "resourceProvider");, consider using the actual parameter name "athenzResourceProvider" to match the coding style elsewhere.
  • For the single‑arg overload, you might clarify in Javadoc that the default resourceTagValue is "*" and briefly mention that resourceTagValue is used only for metric tagging, not authorization semantics.

These are purely cosmetic; the behavior is fine as‑is. As per coding guidelines, this keeps API usage and metrics behavior clearer.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1aad9a1 and 8b1fb2d.

📒 Files selected for processing (1)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (1)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)

24-58: Resource provider fields and defaults are coherent

The introduction of DEFAULT_RESOURCE_TAG_VALUE, athenzResourceProvider, and resourceTagValue fits the new dynamic-resource design; the @Nullable usage plus later checkState calls in newDecorator() keep the builder’s state consistent. No changes needed here.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)

77-84: Document the default resource tag value in javadoc.

The javadoc should mention that this convenience method uses a default resourceTagValue of "*" for metric tagging. Users need to understand the implications of using the default versus providing an explicit tag value.

Consider updating the javadoc:

 /**
  * Sets the {@link AthenzResourceProvider} used to resolve the Athenz resource dynamically for each request.
+ * Uses the default resource tag value {@code "*"} for metrics.
  *
  * <p><strong>Mandatory:</strong> Either this or {@link #resource(String)} must be set before calling {@link #newDecorator()}.</p>
+ *
+ * @see #resourceProvider(AthenzResourceProvider, String)
  */
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b1fb2d and 500e720.

📒 Files selected for processing (1)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (2)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (2)

68-75: LGTM! Static resource correctly wrapped as provider.

The implementation correctly wraps the static resource in an AthenzResourceProvider that returns a completed CompletableFuture, and appropriately sets resourceTagValue to the resource name for metric tagging. The mutual exclusivity check ensures proper builder usage.


138-151: LGTM! Validation and construction logic is solid.

The method correctly validates that all required fields (athenzResourceProvider, athenzAction, resourceTagValue) are set before constructing the AthenzService. The constructor invocation is properly wrapped and passes the provider and tag value as expected for dynamic resource resolution.

JungJaeLee-JJ and others added 2 commits December 10, 2025 14:26
…ServiceBuilder.java

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Comment on lines 72 to 74
return CompletableFuture.completedFuture("");
}
return CompletableFuture.completedFuture(value);
Copy link
Contributor

Choose a reason for hiding this comment

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

  • Conventionally, UnmodifableFuture is used for already completed values.
  • Should we raise AthenzResourceNotFoundException in a error case?
   return UnmodifiableFuture.exceptionallyCompletedFuture(
      new AthenzResourceNotFoundException("Header + headerName + " not found");

Copy link
Author

Choose a reason for hiding this comment

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

I've also updated the return type from CompletableFuture to UnmodifiableFuture and made it throw AthenzResourceNotFoundException when the resource cannot be resolved.

Comment on lines 79 to 80
@UnstableApi
public class JsonBodyFieldAthenzResourceProvider implements AthenzResourceProvider {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
@UnstableApi
public class JsonBodyFieldAthenzResourceProvider implements AthenzResourceProvider {
final class JsonBodyFieldAthenzResourceProvider implements AthenzResourceProvider {

Copy link
Author

Choose a reason for hiding this comment

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

Done! Removed public modifiers from all AthenzResourceProvider implementations.

Comment on lines 49 to 50
@UnstableApi
public class HeaderAthenzResourceProvider implements AthenzResourceProvider {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's exclude the implementations from public API.

Suggested change
@UnstableApi
public class HeaderAthenzResourceProvider implements AthenzResourceProvider {
final class HeaderAthenzResourceProvider implements AthenzResourceProvider {

Copy link
Author

Choose a reason for hiding this comment

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

Done! Removed public modifiers from all AthenzResourceProvider implementations.

return new JsonBodyFieldAthenzResourceProvider(jsonPointer);
}

static AthenzResourceProvider ofJsonField(String jsonFieldName) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind writing Javadoc for the public methods?

Copy link
Author

Choose a reason for hiding this comment

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

Also added Javadoc for the public methods in AthenzResourceProvider.java.

public CompletableFuture<String> provide(ServiceRequestContext ctx, HttpRequest req) {
final MediaType contentType = req.contentType();
if (contentType == null || !contentType.is(MediaType.JSON)) {
return CompletableFuture.completedFuture("");
Copy link
Contributor

Choose a reason for hiding this comment

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

How about returning an exception instead to describe the exception in detail?

if (contentType == null || !contentType.is(MediaType.JSON)) {
   return UnmodifiableFuture.exceptionallyCompletedFuture(
      new AthenzResourceNotFoundException("Unsupported media type. Only " +
          "application/json are supported");
}

Copy link
Author

Choose a reason for hiding this comment

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

Changed to return AthenzResourceNotFoundException

final JsonNode root = mapper.readTree(agg.contentUtf8());
final JsonNode node = root.at(jsonPointer);
if (node.isMissingNode()) {
logger.debug("JSON pointer '{}' not found in request body", jsonPointer);
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto. Should we throw AthenzResourceNotFoundException and include the exception in the returned response body?

Copy link
Author

@JungJaeLee-JJ JungJaeLee-JJ Dec 10, 2025

Choose a reason for hiding this comment

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

I've introduced AthenzResourceNotFoundException and updated all provider implementations to throw this exception when:

  • Resource resolution fails
  • Resolved resource is null or empty string

The exception is caught in AthenzService.serve() and included in the error response body.

Changes in 5745e58

Comment on lines 55 to 56
@UnstableApi
public final class PathAthenzResourceProvider implements AthenzResourceProvider {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
@UnstableApi
public final class PathAthenzResourceProvider implements AthenzResourceProvider {
final class PathAthenzResourceProvider implements AthenzResourceProvider {

Copy link
Author

Choose a reason for hiding this comment

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

Done! Removed public modifiers from all AthenzResourceProvider implementations.

"Exception occurred while resolving resource: " + cause.getMessage()));
}

if (athenzResource == null || athenzResource.isEmpty()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we handle AthenzResourceNotFoundException to return FORBIDDEN?

Copy link
Author

Choose a reason for hiding this comment

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

Done!

    private static HttpResponse createErrorResponse(Throwable cause) {
        if (cause instanceof AthenzResourceNotFoundException) {
            return HttpResponse.of(HttpStatus.FORBIDDEN, MediaType.PLAIN_TEXT,
                                   "Resource could not be resolved: " + cause.getMessage());
        } else {
            return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, MediaType.PLAIN_TEXT,
                                   "Exception occurred while resolving resource: " + cause.getMessage());
        }
    }

return HttpResponse.of(HttpStatus.UNAUTHORIZED, MediaType.PLAIN_TEXT, status.toString());
}
}, ctx.blockingTaskExecutor());
final CompletableFuture<HttpResponse> future = athenzResourceProvider.provide(ctx, req)
Copy link
Contributor

Choose a reason for hiding this comment

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

athenzResourceProvider.provide(ctx, req) is a user code that may throw exceptions. Let's wrap it with a try-catch block.

Copy link
Author

Choose a reason for hiding this comment

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

Wrap it with try-catch block!

       try {
            final CompletableFuture<String> resourceFuture = athenzResourceProvider.provide(ctx, req);
            final CompletableFuture<HttpResponse> future = resourceFuture
                    .thenApplyAsync(
                            athenzResource -> {
                                if (athenzResource == null || athenzResource.isEmpty()) {
                                    throw new AthenzResourceNotFoundException(
                                            "Athenz resource could not be resolved.");
                                }
                                final AccessCheckStatus status = authZpeClient.allowAccess(token,
                                                                                           athenzResource,
                                                                                           athenzAction);
                                final long elapsedNanos = System.nanoTime() - startNanos;
                                if (status == AccessCheckStatus.ALLOW) {
                                    assert allowedTimer != null;
                                    allowedTimer.record(elapsedNanos, TimeUnit.NANOSECONDS);
                                    try {
                                        return unwrap().serve(ctx, req);
                                    } catch (Exception e) {
                                        return Exceptions.throwUnsafely(e);
                                    }
                                } else {
                                    assert deniedTimer != null;
                                    deniedTimer.record(elapsedNanos, TimeUnit.NANOSECONDS);
                                    return HttpResponse.of(HttpStatus.UNAUTHORIZED, MediaType.PLAIN_TEXT,
                                                           status.toString());
                                }
                            },
                            ctx.blockingTaskExecutor())
                    .exceptionally(cause -> {
                        final Throwable unwrapped = Exceptions.peel(cause);
                        assert deniedTimer != null;
                        deniedTimer.record(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
                        return createErrorResponse(unwrapped);
                    });
            return HttpResponse.of(future);
        } catch (Exception e) {
            assert deniedTimer != null;
            deniedTimer.record(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
            return createErrorResponse(e);
        }

@ikhoon
Copy link
Contributor

ikhoon commented Dec 10, 2025

Would you run ./gradlew checkstyle to verify any style violations?

- Remove unnecessary try-catch blocks for sync/async separation
- Consolidate exception handling into single `.exceptionally()` block
- Remove redundant `supplyAsync()` wrapping of already-async operations
- Reduce thread context switching by removing unnecessary `thenCompose()`
- Fix Checkstyle violations:
  * Line length exceeds in Javadoc comments
  * Replace `CompletableFuture.completedFuture()` with `UnmodifiableFuture.completedFuture()`
  * Add missing copyright header to test files

Performance improvements:
- Reduced thread transitions from 4 to 2 for PathAthenzResourceProvider
- Eliminated unnecessary executor usage for already-completed futures
- More efficient resource resolution pipeline

This change maintains the same functionality while improving:
- Code readability and maintainability
- Performance through reduced thread context switching
- Consistent error handling across sync and async paths
@JungJaeLee-JJ JungJaeLee-JJ changed the title Add AthenzResourceProvider for dynamic resource resolution Add AthenzResourceProvider for dynamic resource resolution Dec 10, 2025
@JungJaeLee-JJ JungJaeLee-JJ changed the title Add AthenzResourceProvider for dynamic resource resolution Add AthenzResourceProvider for dynamic resource resolution Dec 10, 2025
@JungJaeLee-JJ JungJaeLee-JJ changed the title Add AthenzResourceProvider for dynamic resource resolution Add AthenzResourceProvider for dynamic athenz resource resolution Dec 10, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)

132-136: tokenType(TokenType...) currently ignores its argument

The varargs overload validates input but never updates this.tokenTypes, so:

builder.tokenType(TokenType.ACCESS_TOKEN);

has no effect, and the builder continues to use the default list. That’s surprising and likely not what callers expect.

Consider assigning the field, mirroring the Iterable overload:

     public AthenzServiceBuilder tokenType(TokenType... tokenTypes) {
         requireNonNull(tokenTypes, "tokenTypes");
         checkArgument(tokenTypes.length > 0, "tokenTypes must not be empty");
-        return this;
+        this.tokenTypes = ImmutableList.copyOf(tokenTypes);
+        return this;
     }
🧹 Nitpick comments (2)
athenz/src/test/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProviderTest.java (1)

66-82: Fix typos in test names for clarity

The method names use “Exits” instead of “Exists”, e.g.:

  • shouldProvideHeaderStringIfExits
  • shouldThrowExceptionIfHeaderNotExits
  • shouldProvideJsonFieldStringIfExits
  • shouldProvideJsonFieldStringIfExitsByJsonPointer
  • shouldThrowExceptionIfJsonFieldNotExits

Renaming them to ...IfExists / ...NotExists would improve readability while keeping behavior unchanged.

Also applies to: 96-112, 114-163

athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)

63-69: Align resource() Javadoc with both resourceProvider overloads

The Javadoc for resource(String) references only
resourceProvider(AthenzResourceProvider, String) as the alternative mandatory configuration:

 * <p><strong>Mandatory:</strong> Either this or
 * {@link #resourceProvider(AthenzResourceProvider, String)} must be set ...

But you also provide a no-tag overload resourceProvider(AthenzResourceProvider) that delegates to the 2‑arg method. To avoid confusion, it’s clearer to mention resourceProvider(AthenzResourceProvider) (or just “resourceProvider(...)”) in this Javadoc as well.

Purely a doc tweak; no behavior change required.

Also applies to: 80-91

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 500e720 and 5745e58.

📒 Files selected for processing (11)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzResourceNotFoundException.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (5 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (3 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProvider.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/package-info.java (1 hunks)
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzDocker.java (2 hunks)
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java (1 hunks)
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProviderTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzDocker.java
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/package-info.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProvider.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProviderTest.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzResourceNotFoundException.java
🧬 Code graph analysis (3)
athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java (2)
athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzDocker.java (1)
  • AthenzDocker (55-340)
core/src/main/java/com/linecorp/armeria/common/util/UnmodifiableFuture.java (1)
  • UnmodifiableFuture (42-212)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (4)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzResourceNotFoundException.java (1)
  • UnstableApi (25-42)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (1)
  • UnstableApi (111-249)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1)
  • UnstableApi (48-74)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1)
  • UnstableApi (78-129)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzResourceNotFoundException.java (3)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)
  • UnstableApi (41-167)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1)
  • UnstableApi (48-74)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1)
  • UnstableApi (78-129)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (6)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/package-info.java (1)

1-25: Package-level annotations and Javadoc look correct

License header, package Javadoc, and the use of @UnstableApi and @NonNullByDefault at the package level all look consistent with Armeria conventions. This neatly marks the new resource provider APIs as unstable and enforces non-null-by-default semantics for everything under this package.

As per coding guidelines, this satisfies the requirement to treat newly added public APIs as unstable.

athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzResourceNotFoundException.java (1)

21-41: Exception type and API shape look good

AthenzResourceNotFoundException is small, focused, correctly annotated with @UnstableApi, and provides the expected constructors for providers and AthenzService to use. No changes needed.

athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (1)

50-72: Path-based provider implementation matches contract

Using ctx.path() for the no-query case and req.path() for the with-query case cleanly matches the documented examples and the ofPath(boolean includeQuery) factories. Looks correct and minimal.

athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProvider.java (1)

28-76: Resource provider interface and factories look well-shaped

AthenzResourceProvider is a clear @UnstableApi functional interface with concise factories (ofPath, ofHeader, ofJsonField variants) that map cleanly to the internal implementations. The async CompletableFuture<String> contract fits the dynamic-resolution use cases.

No changes needed here.

Also applies to: 78-125

athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java (1)

57-131: Integration wiring comprehensively exercises the new providers

The server setup and tests collectively cover:

  • Path-based, header-based, and JSON-body resource resolution.
  • Valid vs invalid resources across all TokenType values.
  • Provider exception, null, and empty-resource scenarios mapping to FORBIDDEN.

The decorators are configured using the new resourceProvider(...) API and behave consistently with AthenzService’s documented error handling. Looks solid.

Also applies to: 134-302

athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (1)

49-67: Dynamic resource resolution and error handling are consistent and robust

The refactor to use AthenzResourceProvider plus resourceTagValue:

  • Keeps the static-resource constructor behavior by wrapping into a provider.
  • Ensures metrics use a stable resourceTagValue to avoid cardinality issues.
  • Properly distinguishes:
    • 401 for missing token / denied access,
    • 403 for AthenzResourceNotFoundException (unresolvable resources),
    • 500 for other provider/Resolution exceptions,
      while always recording either allowedTimer or deniedTimer.

The async chain with thenApplyAsync(..., ctx.blockingTaskExecutor()) and the fallback try/catch around provide() give good coverage for both async and synchronous provider failures. No changes needed.

Also applies to: 122-157, 174-225, 240-247

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1)

41-42: Update Javadoc to reflect exceptional completion behavior.

The Javadoc states "an empty string is returned and the error is logged at debug level," but the implementation (lines 109-111, 121, 125) throws AthenzResourceNotFoundException which causes the future to complete exceptionally. There is no empty-string fallback.

Update the documentation to reflect the actual behavior:

  * <p>This provider parses the request body as JSON and extracts the resource value from the field
- * specified by a JSON pointer. If the content type is not JSON, the field does not exist,
- * or parsing fails, an empty string is returned and the error is logged at debug level.
+ * specified by a JSON pointer. If the content type is not JSON, the field does not exist,
+ * or parsing fails, the returned future completes exceptionally with
+ * {@link AthenzResourceNotFoundException}.

Note: This issue was previously identified in past review comments.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5745e58 and 26a35c6.

📒 Files selected for processing (6)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (5 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceNotFoundException.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1 hunks)
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1 hunks)
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java (1 hunks)
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProviderTest.java (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceNotFoundException.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java
  • athenz/src/test/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProviderTest.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java
🧬 Code graph analysis (2)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceNotFoundException.java (3)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (1)
  • UnstableApi (112-250)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1)
  • UnstableApi (47-73)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (1)
  • UnstableApi (77-128)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (5)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (1)
  • UnstableApi (112-250)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceNotFoundException.java (1)
  • UnstableApi (25-42)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (1)
  • UnstableApi (47-73)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (1)
  • UnstableApi (56-73)
athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (1)
  • UnstableApi (41-167)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (11)
athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceNotFoundException.java (1)

26-42: LGTM!

The exception class is well-structured with proper @UnstableApi annotation, clear documentation, and standard exception constructors.

athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (4)

53-111: LGTM!

The enhanced documentation clearly explains both static and dynamic resource patterns with practical examples and comprehensive error-handling guidance.


135-158: LGTM!

The constructor design cleanly supports both dynamic providers and static resources while maintaining backward compatibility by wrapping static resources in a provider.


186-227: LGTM!

The asynchronous resource resolution logic is well-implemented with comprehensive error handling:

  • Try-catch for synchronous provider exceptions
  • exceptionally handler for async failures
  • Null/empty resource validation
  • Proper timer recording in all code paths

All concerns from previous reviews have been addressed.


241-249: LGTM!

The error response helper correctly distinguishes between resource-not-found scenarios (403 FORBIDDEN) and unexpected exceptions (500 INTERNAL_SERVER_ERROR), as specified in the PR objectives.

athenz/src/test/java/com/linecorp/armeria/server/athenz/resource/AthenzResourceProviderTest.java (1)

36-162: LGTM!

Comprehensive test coverage for all provider types (path, header, JSON field) with clear given-when-then structure and proper validation of both success and error paths.

athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (2)

57-61: LGTM!

Constructor properly validates the header name parameter, consistent with validation patterns in other provider implementations.


64-72: LGTM!

The provide method correctly returns an exceptionally completed future with AthenzResourceNotFoundException for missing or empty headers, and uses UnmodifiableFuture appropriately.

athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (2)

89-103: LGTM!

Both constructors properly validate inputs and support flexible field specification through simple names or JSON Pointers, with dot-notation conversion for convenience.


106-127: LGTM!

The implementation correctly validates JSON content-type before aggregation, properly parses the JSON body, and throws AthenzResourceNotFoundException for all error cases (unsupported content-type, missing field, parsing failures).

athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzResourceProviderIntegrationTest.java (1)

52-303: LGTM!

Comprehensive integration test suite that validates:

  • All provider types (path, header, JSON body)
  • Authorization success and denial scenarios
  • Error handling (exceptions, null, empty resources)
  • Correct HTTP status codes (OK, UNAUTHORIZED, FORBIDDEN)

Parameterized tests over TokenType ensure thorough coverage.

@JungJaeLee-JJ
Copy link
Author

Would you run ./gradlew checkstyle to verify any style violations?

Sorry for bothering you multiple times due to the code conventions. I ran ./gradlew :athenz:checkstyleMain and confirmed that it passes successfully.

@JungJaeLee-JJ JungJaeLee-JJ changed the title Add AthenzResourceProvider for dynamic athenz resource resolution Add AthenzResourceProvider for dynamic athenz resource resolution Dec 10, 2025
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.

4 participants